From 34d81c66a866a3d2007d50f81cfed6bedd5a1c2d Mon Sep 17 00:00:00 2001 From: Cyber Mawonaj Date: Sat, 25 Jul 2026 22:34:22 -0400 Subject: [PATCH] Phases 6 et 7 : application SvelteKit, PWA, systemd, documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Application — rendu serveur sur adapter-node, conforme au §3bis : - tableau de bord, recherche à facettes, fiche détaillée, calendrier - /a-propos et /methode prérendues ; tout le reste en rendu serveur - API JSON ouverte : /api/textes, /api/textes/[slug], /api/echeances - recherche FTS5 avec surlignage et classement bm25 pondéré (le titre pèse dix fois plus que les points clés) - facettes recalculées sur le résultat filtré par les AUTRES facettes : cocher un thème doit recompter les statuts encore disponibles, sinon les compteurs mentent Identité OKI sobrifiée : tokens en CSS vanilla, thème sombre par défaut et clair opt-in, or comme seule couleur d'action, flag-bar une fois par écran, polices Archivo/Inter auto-hébergées. Politique de sécurité de contenu interdisant toute requête tierce. Les statuts ne reposent jamais sur la seule couleur : libellé en toutes lettres et pastille de forme distincte. Budget respecté très largement : 6,6 Ko de JavaScript sur l'accueil (3 Ko compressés) pour un plafond de 100 Ko. Les faits clés — AFD à 500 €, saisine 2026-915 DC, statut — sont lisibles sans JavaScript. Trois défauts trouvés en testant l'application, pas en relisant le code : - v_textes est une vue, et une vue SQLite n'a pas de rowid : la jointure avec l'index plein texte échouait (migration 005) - ppl-montagne remontait en « pertinence forte » alors que le corpus dit « sans portée pour la Guadeloupe » — la cotation comptait le mot sans lire la négation. Idem pour pjl-logement et accord-globe. Corrigé par une lecture du voisinage, avec quatre tests de non-régression. - les libellés d'affichage étaient importés depuis /server dans un composant, ce que SvelteKit interdit à raison PWA : service worker réseau-d'abord pour les pages, cache-d'abord pour le coffre du build. Une veille législative ne doit pas servir une page périmée quand le réseau répond — une date de promulgation change tout. Les réponses issues du cache portent un en-tête qui le dit. systemd : timer du pipeline (6 h, 18 h, dimanche 9 h, avec dispersion et rattrapage), application activée par socket avec mise en sommeil à 300 s. Unités durcies, validées par systemd-analyze. Documentation : README d'installation sur Debian/Ubuntu vierge, README-pipeline avec le piège des deux couples d'identifiants PISTE, et scripts/verifier-conformite.sh qui contrôle mécaniquement le §3bis et le §8. 172 tests, npm run check à 0 erreur, make verifier au vert. Co-Authored-By: Claude Opus 5 --- README-pipeline.md | 209 ++ README.md | 280 +- pipeline/guadeloupe.py | 80 +- pipeline/migrations/005_vue_rowid.sql | 34 + scripts/verifier-conformite.sh | 103 + systemd/veille-legislative.service | 47 + systemd/veille-legislative.timer | 26 + systemd/veille-web.service | 52 + systemd/veille-web.socket | 17 + tests/test_update.py | 55 + web/.gitignore | 4 + web/.npmrc | 3 + web/package-lock.json | 2304 +++++++++++++++++ web/package.json | 32 + web/scripts/copier-polices.js | 33 + web/src/app.html | 35 + web/src/lib/composants/BadgeGuadeloupe.svelte | 48 + web/src/lib/composants/BadgeStatut.svelte | 71 + web/src/lib/composants/CarteTexte.svelte | 129 + web/src/lib/composants/PanneauFacettes.svelte | 159 ++ web/src/lib/format.ts | 47 + web/src/lib/libelles.ts | 94 + web/src/lib/server/db.ts | 53 + web/src/lib/server/requetes.ts | 573 ++++ web/src/lib/styles/base.css | 342 +++ web/src/lib/styles/oki-tokens.css | 104 + web/src/lib/types.ts | 180 ++ web/src/routes/+error.svelte | 57 + web/src/routes/+layout.server.ts | 9 + web/src/routes/+layout.svelte | 220 ++ web/src/routes/+page.server.ts | 26 + web/src/routes/+page.svelte | 362 +++ web/src/routes/a-propos/+page.svelte | 121 + web/src/routes/a-propos/+page.ts | 5 + web/src/routes/api/echeances/+server.ts | 26 + web/src/routes/api/textes/+server.ts | 36 + web/src/routes/api/textes/[slug]/+server.ts | 16 + web/src/routes/calendrier/+page.server.ts | 22 + web/src/routes/calendrier/+page.svelte | 209 ++ web/src/routes/methode/+page.svelte | 206 ++ web/src/routes/methode/+page.ts | 2 + web/src/routes/recherche/+page.server.ts | 34 + web/src/routes/recherche/+page.svelte | 215 ++ web/src/routes/textes/[slug]/+page.server.ts | 19 + web/src/routes/textes/[slug]/+page.svelte | 571 ++++ web/src/service-worker.ts | 121 + web/static/favicon.svg | 10 + web/static/manifest.webmanifest | 17 + web/static/theme.js | 19 + web/svelte.config.js | 39 + web/tsconfig.json | 14 + web/vite.config.ts | 9 + 52 files changed, 7459 insertions(+), 40 deletions(-) create mode 100644 README-pipeline.md create mode 100644 pipeline/migrations/005_vue_rowid.sql create mode 100644 scripts/verifier-conformite.sh create mode 100644 systemd/veille-legislative.service create mode 100644 systemd/veille-legislative.timer create mode 100644 systemd/veille-web.service create mode 100644 systemd/veille-web.socket create mode 100644 web/.gitignore create mode 100644 web/.npmrc create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/scripts/copier-polices.js create mode 100644 web/src/app.html create mode 100644 web/src/lib/composants/BadgeGuadeloupe.svelte create mode 100644 web/src/lib/composants/BadgeStatut.svelte create mode 100644 web/src/lib/composants/CarteTexte.svelte create mode 100644 web/src/lib/composants/PanneauFacettes.svelte create mode 100644 web/src/lib/format.ts create mode 100644 web/src/lib/libelles.ts create mode 100644 web/src/lib/server/db.ts create mode 100644 web/src/lib/server/requetes.ts create mode 100644 web/src/lib/styles/base.css create mode 100644 web/src/lib/styles/oki-tokens.css create mode 100644 web/src/lib/types.ts create mode 100644 web/src/routes/+error.svelte create mode 100644 web/src/routes/+layout.server.ts create mode 100644 web/src/routes/+layout.svelte create mode 100644 web/src/routes/+page.server.ts create mode 100644 web/src/routes/+page.svelte create mode 100644 web/src/routes/a-propos/+page.svelte create mode 100644 web/src/routes/a-propos/+page.ts create mode 100644 web/src/routes/api/echeances/+server.ts create mode 100644 web/src/routes/api/textes/+server.ts create mode 100644 web/src/routes/api/textes/[slug]/+server.ts create mode 100644 web/src/routes/calendrier/+page.server.ts create mode 100644 web/src/routes/calendrier/+page.svelte create mode 100644 web/src/routes/methode/+page.svelte create mode 100644 web/src/routes/methode/+page.ts create mode 100644 web/src/routes/recherche/+page.server.ts create mode 100644 web/src/routes/recherche/+page.svelte create mode 100644 web/src/routes/textes/[slug]/+page.server.ts create mode 100644 web/src/routes/textes/[slug]/+page.svelte create mode 100644 web/src/service-worker.ts create mode 100644 web/static/favicon.svg create mode 100644 web/static/manifest.webmanifest create mode 100644 web/static/theme.js create mode 100644 web/svelte.config.js create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/README-pipeline.md b/README-pipeline.md new file mode 100644 index 0000000..9e28819 --- /dev/null +++ b/README-pipeline.md @@ -0,0 +1,209 @@ +# Pipeline de veille — installation, clés, dépannage + +Le pipeline fait deux choses : **remplir** la base depuis le corpus de recherche +(`make seed`), puis la **tenir à jour** depuis les sources officielles +(`make update`). Il n'écrit jamais dans `data/input/`, qui est en lecture seule. + +--- + +## 1. Installation + +```bash +uv venv --python 3.12 +uv pip install -e ".[dev]" +cp .env.example .env # puis renseigner les clés, voir §2 +make seed # remplit data/veille.db +``` + +`make seed` est idempotent : le relancer ne duplique rien. Pour repartir d'une +base vierge : `make reseed`. + +Le compte-rendu affiché en fin de seed est le vrai livrable : il dit combien de +textes sont entrés, combien de citations ont été résolues, et — surtout — ce qui +manque. + +--- + +## 2. Clés d'API + +### Légifrance, via PISTE + +Créez un compte sur [piste.gouv.fr](https://piste.gouv.fr), déclarez une +application, et **abonnez-la à l'API « Légifrance »**. + +> **Le piège à connaître.** Une application PISTE affiche *deux* couples de +> valeurs. Seul le couple **OAuth** fonctionne ici : +> +> | Ce que PISTE affiche | À mettre dans `.env` | +> |---|---| +> | **Client ID** / **Client secret** | ✅ `LEGIFRANCE_CLIENT_ID` / `LEGIFRANCE_CLIENT_SECRET` | +> | API key / API key secret | ❌ produit `invalid_client` à la demande de jeton | +> +> Vérifié le 25 juillet 2026 sur un compte réel : la clé d'API est refusée par +> le point d'accès OAuth, quelle que soit la méthode d'authentification tentée +> (corps de requête, en-tête Basic, avec ou sans `scope`). + +Second piège : **les identifiants de production ne fonctionnent pas en bac à +sable.** `sandbox` exige une application distincte, déclarée sur l'environnement +de test. Laissez `LEGIFRANCE_ENV=prod` sauf si vous avez explicitement créé +cette seconde application. + +Vérification en une commande : + +```bash +.venv/bin/python -c " +from pipeline.collecteurs.http import ClientHttp +from pipeline.collecteurs.legifrance import CollecteurLegifrance +with ClientHttp() as c: + col = CollecteurLegifrance(c, numeros_a_verifier=['2026-491']) + print(col.est_disponible()) + print(col.collecter().textes) +" +``` + +### Aucune autre clé n'est nécessaire + +Le Sénat et le Conseil constitutionnel sont interrogés sans authentification. + +--- + +## 3. Ce qui marche sans clé PISTE + +C'est le point important : **le pipeline reste utile sans Légifrance.** + +| Collecteur | Sans clé PISTE | Avec clé PISTE | +|---|---|---| +| `legifrance` | ⛔ désactivé, motif affiché dans le rapport | ✅ vérifie chaque numéro de loi en base | +| `senat` | ✅ liste chronologique des lois promulguées | ✅ idem, sert de contrôle croisé | +| `conseil_constitutionnel` | ✅ affaires en instance + décisions rendues | ✅ idem | + +Ce qui est **perdu** sans clé : la confirmation de l'intitulé officiel et de la +date de signature directement au Journal officiel, et l'identifiant JORF de +chaque texte. Ce qui est **conservé** : la détection des promulgations +nouvelles (par le Sénat) et le suivi des décisions constitutionnelles — soit +l'essentiel de ce qui fait bouger un statut. + +Le rapport de run indique explicitement quand un collecteur est ignoré : + +``` +Collecteurs + legifrance ignoré — LEGIFRANCE_CLIENT_ID absent de .env — repli sur + l'Assemblée nationale, le Sénat et le Conseil constitutionnel + senat 31 texte(s), 0 décision(s) (0.4 s) +``` + +### Une URL du cahier des charges est morte + +Le §5.1 indique `senat.fr/lois/index.html` : cette adresse renvoie **404** depuis +la refonte du site. L'équivalent fonctionnel, utilisé par le collecteur, est +`senat.fr/dossiers-legislatifs/lois-promulguees.html`. + +De même, `assemblee-nationale.fr/dyn/actualites-accueil/promulgations-de-lois` +renvoie 404. La liste du Sénat couvrant l'intégralité des lois promulguées, elle +tient lieu de source unique pour les promulgations. + +--- + +## 4. Utilisation + +```bash +make update-dry # collecte réelle, aucune écriture, rapport des écarts +make update # collecte et applique +``` + +**Toujours commencer par `--dry-run`.** Le rapport liste chaque écart avec sa +nature, son ancienne et sa nouvelle valeur : + +``` +Écarts détectés : 11 + ajouts : 7 + modifications : 4 + signalements : 0 + + [ajout ] (nouveau) 2026-103 · LOI n° 2026-103 du 19 février 2026 de finances + [decision_cc ] ppl-aide-a-mourir 2026-910 DC · date_saisine : — → 2026-07-16 + [decision_cc ] loi-2026-650 2026-908 DC · date_decision : — → 2026-07-23 +``` + +Options utiles : + +| Option | Effet | +|---|---| +| `--dry-run` | collecte réelle, aucune écriture | +| `--hors-ligne` | n'utilise que le cache HTTP, sans accès réseau | +| `--verbeux` | journalisation détaillée, requête par requête | + +--- + +## 5. Automatisation + +### systemd (recommandé) + +```bash +sudo cp systemd/veille-legislative.{service,timer} /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now veille-legislative.timer +systemctl list-timers veille-legislative.timer +``` + +Cadence : 6 h et 18 h tous les jours, plus un passage le dimanche à 9 h pour +rattraper les publications de fin de semaine. `RandomizedDelaySec=900` évite de +frapper Légifrance à la seconde ronde. `Persistent=true` rattrape le passage +manqué si la machine était éteinte. + +### cron (repli documenté) + +```cron +0 6,18 * * * cd /opt/veille-legislative && .venv/bin/python -m pipeline.update >> /var/log/veille.log 2>&1 +0 9 * * 0 cd /opt/veille-legislative && .venv/bin/python -m pipeline.update >> /var/log/veille.log 2>&1 +``` + +Le repli cron perd le durcissement de sécurité des unités systemd et la reprise +des passages manqués. + +--- + +## 6. Dépannage + +| Symptôme | Cause probable | Remède | +|---|---|---| +| `invalid_client` sur le jeton | clé d'API utilisée au lieu du couple OAuth | voir §2 | +| `invalid_client` avec le bon couple | application non abonnée à l'API Légifrance | demander l'abonnement sur piste.gouv.fr, compter 24-48 h | +| 403 sur `/search` alors que le jeton est obtenu | abonnement en cours de propagation | réessayer plus tard | +| `Base introuvable` | seed jamais lancé | `make seed` | +| `no such column: t.rowid` | base créée avant la migration 005 | `make db-init` (les migrations s'appliquent seules) | +| Collecte lente | premier passage, cache vide | normal : ~25 s pour 27 numéros, quasi instantané ensuite | +| Un collecteur en échec | site indisponible ou structure changée | le run continue ; vérifier le rapport et les fixtures de `tests/fixtures/` | + +### Le cache HTTP + +Les réponses sont mises en cache dans `data/cache_http/` pendant +`VEILLE_CACHE_TTL_H` heures (6 par défaut). Pour forcer une collecte fraîche : + +```bash +rm -rf data/cache_http +``` + +### Sauvegarde + +La base est un fichier unique. Sauvegarder revient à le copier — mais **jamais +pendant un passage**, le mode WAL laissant des fichiers annexes : + +```bash +sqlite3 data/veille.db ".backup data/veille-$(date +%F).db" +``` + +--- + +## 7. Tests + +```bash +make test # pytest avec couverture +make lint # ruff +``` + +Aucun test ne touche au réseau : les collecteurs sont éprouvés sur des fixtures +HTML figées dans `tests/fixtures/`, capturées le 25 juillet 2026. **Si un test +de fixture échoue après une mise à jour, c'est le site officiel qui a changé de +structure** — c'est exactement ce que ces tests servent à détecter. Recapturez +alors la fixture et adaptez le parseur. diff --git a/README.md b/README.md index ee1b05b..a942bf4 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,259 @@ -# Veille législative Guadeloupe +# Veille législative Gwadloup Plateforme de veille sur la production législative française — lois, projets et -propositions de loi, ordonnances, décisions du Conseil constitutionnel — analysée -sous l'angle des **libertés des individus, des associations et des entreprises**, -avec un zoom Guadeloupe et outre-mer. - -Deux composantes indissociables : - -1. **`pipeline/`** — automatisation Python. Ingère le corpus de recherche - (date d'arrêté : 25 juillet 2026, périmètre 1ᵉʳ mai → 30 septembre 2026) puis - met la base à jour depuis les sources officielles. -2. **`web/`** — application SvelteKit rendue côté serveur : recherche plein - texte, filtres à facettes partageables, calendrier des échéances, fiches - détaillées avec sources et extraits verbatim. +propositions de loi, ordonnances, décisions du Conseil constitutionnel — lue +sous l'angle des **libertés des individus, des associations et des +entreprises**, avec un zoom Guadeloupe et outre-mer. > **Aucune donnée législative n'est inventée.** Chaque fait porte sa source -> (URL, date, extrait), son niveau de confiance et son fichier d'origine. Ce qui -> n'est pas établi est marqué « à vérifier » plutôt que deviné. +> (adresse, date, extrait verbatim), son niveau de confiance et son fichier +> d'origine. Ce qui n'est pas établi est marqué « à vérifier », avec son motif +> affiché sur la fiche. -## État d'avancement +--- -| Phase | Objet | Statut | -|---|---|---| -| 0 | Lecture du corpus, plan d'implémentation | ✅ `PLAN-IMPLEMENTATION.md` | -| 1 | Socle : schéma SQLite + FTS5, modèles | ✅ | -| 2 | Parseur des tableaux récapitulatifs | ✅ 52 textes | -| 3 | Parseurs des rapports de dimension, citations | ✅ 360 sources | -| 4 | Enrichissement et tests d'acceptation | ✅ 7/7 critères §7 | -| 5 | Collecteurs officiels, run de mise à jour | 🚧 | -| 6 | Application web | ⏳ | -| 7 | PWA, systemd, documentation d'installation | ⏳ | - -## Ce que contient la base après `make seed` +## Ce que contient la base | | | |---|---| -| Textes législatifs | **52** (27 promulgués, 7 devant le Conseil constitutionnel, 10 en navette, 5 déposés, 2 annoncés, 1 validé) | -| Sources vérifiables | **360**, toutes avec URL — aucun texte sans lien | -| Événements de timeline | 83 | +| Textes législatifs | **52** — 27 promulgués, 7 devant le Conseil constitutionnel, 10 en navette, 5 déposés, 2 annoncés, 1 validé | +| Sources vérifiables | **360**, toutes avec adresse — aucun texte sans lien | +| Événements de parcours | 83 | | Affaires du Conseil constitutionnel | 12 | | Analyses transversales | 7 | -| Échéances du calendrier | 9 | -| Marqueurs de citation résolus | 100 % (332/332), dont 93 % avec un lien | +| Échéances de calendrier | 9 | +| Marqueurs de citation résolus | **100 %** (332/332), dont 93 % avec un lien | Les valeurs déduites — 16 cotations Guadeloupe, 17 ventilations d'impact — sont -marquées « à vérifier » avec leur motif, et affichées comme telles. +signalées comme telles, avec leur motif. -L'installation complète sur Debian/Ubuntu vierge est documentée en phase 7. +--- + +## Architecture + +``` +veille_legislative_971/ +├── data/input/ corpus de recherche — LECTURE SEULE, jamais modifié +├── pipeline/ Python 3.12 : parseurs, collecteurs, run de veille +├── web/ SvelteKit 2 + Svelte 5, rendu serveur sur adapter-node +├── systemd/ 4 unités : timer du pipeline, socket + service de l'app +├── tests/ pytest, fixtures HTML figées — aucun test ne va au réseau +└── scripts/ contrôles de conformité +``` + +Une seule base SQLite, `data/veille.db`. La recherche plein texte est assurée +par SQLite lui-même (FTS5). Aucun service externe n'est nécessaire au +fonctionnement : ni moteur de recherche, ni réseau de diffusion, ni traceur. + +--- + +## Installation sur Debian ou Ubuntu vierge + +Testé sur Debian 13 et Ubuntu 24.04. + +### 1. Dépendances système + +```bash +sudo apt update +sudo apt install -y git curl build-essential sqlite3 ca-certificates +``` + +### 2. Node.js ≥ 20.6 + +La version 20.6 est le minimum : l'application utilise `node --env-file`, qui +n'existe pas avant. + +```bash +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt install -y nodejs +node --version # doit afficher v20.6 ou plus +``` + +### 3. Python 3.12 et uv + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +export PATH="$HOME/.local/bin:$PATH" +uv python install 3.12 +``` + +### 4. Le projet + +```bash +sudo mkdir -p /opt/veille-legislative +sudo chown "$USER:$USER" /opt/veille-legislative +git clone /opt/veille-legislative +cd /opt/veille-legislative + +make install # environnement Python + dépendances npm +cp .env.example .env +$EDITOR .env # renseigner les identifiants PISTE — voir README-pipeline.md +``` + +### 5. Remplir la base et construire l'application + +```bash +make seed # → 52 textes, 360 sources +cd web && npm run polices && cd .. # copie les polices depuis npm +make build +make verifier # contrôles de conformité +``` + +### 6. Vérifier localement + +```bash +cd web +HOST=127.0.0.1 PORT=3971 VEILLE_DB=/opt/veille-legislative/data/veille.db node build +``` + +Puis, dans un autre terminal : + +```bash +curl -sI http://127.0.0.1:3971/ | head -1 # HTTP/1.1 200 OK +curl -s http://127.0.0.1:3971/api/textes | head -c 200 +``` + +--- + +## Mise en service + +### Compte dédié + +```bash +sudo useradd --system --home /opt/veille-legislative --shell /usr/sbin/nologin veille +sudo chown -R veille:veille /opt/veille-legislative +sudo chmod 600 /opt/veille-legislative/.env +``` + +### Unités systemd + +```bash +sudo cp systemd/*.service systemd/*.timer systemd/*.socket /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now veille-web.socket # l'app se réveille à la demande +sudo systemctl enable --now veille-legislative.timer # collecte 6 h, 18 h, dimanche 9 h +``` + +L'application est **activée par socket** : systemd tient le port ouvert et ne +démarre Node qu'à la première requête ; sans trafic pendant `IDLE_TIMEOUT` +(300 s), elle s'arrête. Sur un site de veille peu fréquenté, l'empreinte mémoire +tombe à zéro entre deux visites — ce qui compte sur un serveur à 5 € par mois. + +Avant de démarrer, ajustez `ORIGIN` dans `veille-web.service` : sans lui, les +soumissions de formulaire sont refusées derrière un reverse proxy. + +### Reverse proxy + +**Caddy** (le plus court, HTTPS automatique) : + +```caddy +veille.example.org { + encode gzip zstd + reverse_proxy 127.0.0.1:3971 +} +``` + +**nginx** : + +```nginx +server { + listen 443 ssl http2; + server_name veille.example.org; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + + location / { + proxy_pass http://127.0.0.1:3971; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + } +} +``` + +Avec nginx, remplacez `ORIGIN` par `PROTOCOL_HEADER=x-forwarded-proto` et +`HOST_HEADER=x-forwarded-host` dans le service. + +### Vérifier + +```bash +systemctl status veille-web.socket +systemctl list-timers veille-legislative.timer +journalctl -u veille-legislative -n 40 +``` + +--- + +## Commandes + +``` +make install installe pipeline Python et application web +make seed remplit la base depuis data/input/ +make reseed repart d'une base vierge +make update-dry collecte réelle, aucune écriture, rapport des écarts +make update collecte et applique +make test pytest avec couverture +make lint ruff +make dev serveur de développement (port 5971) +make build build de production +make check vérification TypeScript +make verifier contrôles de conformité au cahier des charges +``` + +--- + +## Dégradations documentées + +| Situation | Conséquence | +|---|---| +| Sans identifiants PISTE | Le collecteur Légifrance se désactive et l'annonce dans le rapport. Le Sénat et le Conseil constitutionnel prennent le relais : les promulgations et les décisions restent suivies. Voir `README-pipeline.md` §3. | +| Une source indisponible | Le collecteur concerné échoue seul ; les autres poursuivent. L'erreur figure au rapport de run. | +| Réseau coupé | `make update --hors-ligne` travaille sur le cache HTTP. Côté navigateur, le service worker sert les pages déjà consultées, en signalant qu'elles peuvent être périmées. | +| Base absente | L'application refuse de démarrer avec un message indiquant les emplacements essayés. | + +**Deux adresses du cahier des charges sont mortes** et ont été remplacées : +`senat.fr/lois/index.html` et +`assemblee-nationale.fr/dyn/actualites-accueil/promulgations-de-lois` renvoient +toutes deux 404. La liste `senat.fr/dossiers-legislatifs/lois-promulguees.html` +couvre l'ensemble des promulgations et tient lieu de source unique. + +--- + +## Sauvegarde + +La base est un fichier unique : + +```bash +sqlite3 data/veille.db ".backup /sauvegardes/veille-$(date +%F).db" +``` + +Ne copiez jamais le fichier pendant un passage du pipeline : le mode WAL laisse +des fichiers annexes. `.backup` s'en charge correctement. + +--- + +## Interface de programmation + +Ouverte, sans clé : + +| Route | Objet | +|---|---| +| `GET /api/textes` | liste filtrable — `q`, `statut`, `theme`, `type`, `impact`, `guadeloupe`, `confiance`, `from`, `to`, `tri`, `page` | +| `GET /api/textes/[identifiant]` | fiche complète, sources et parcours compris | +| `GET /api/echeances` | calendrier, avec `?jours=N` en option | + +```bash +curl -s 'https://veille.example.org/api/textes?statut=saisie_cc&guadeloupe=forte' | jq '.total' +``` + +--- + +## Licence + +AGPL-3.0-or-later. Le corpus de `data/input/` reste la propriété de ses auteurs. + +Si vous réutilisez ces données, citez la source primaire — Journal officiel, +assemblées, Conseil constitutionnel — plutôt que cette plateforme : elle n'est +qu'un intermédiaire. diff --git a/pipeline/guadeloupe.py b/pipeline/guadeloupe.py index 9e3b74b..75a9dc7 100644 --- a/pipeline/guadeloupe.py +++ b/pipeline/guadeloupe.py @@ -14,7 +14,7 @@ from __future__ import annotations import functools import re -from dataclasses import dataclass +from dataclasses import dataclass, field import yaml @@ -22,6 +22,33 @@ from pipeline import chemins from pipeline.modeles import Pertinence from pipeline.parseurs.dates_fr import sans_accents +# Tournures par lesquelles le corpus **écarte** un territoire qu'il vient de +# nommer : « PPL montagne — sans portée pour la Guadeloupe », « PJL logement — +# pas de volet outre-mer à ce stade ». Compter la mention sans lire la négation +# donnerait à ces textes la cotation la plus forte, à l'exact inverse de ce que +# la source affirme. +NEGATIONS = ( + "sans portee", + "sans objet", + "sans incidence", + "sans specificite", + "sans volet", + "sans consequence", + "ne concerne pas", + "n'est pas concerne", + "pas concernee", + "pas de volet", + "pas d'incidence", + "non concerne", + "hors champ", + "exclue", + "exclus", +) + +# Fenêtre de voisinage, en caractères, autour d'une mention de territoire. +_AVANT = 90 +_APRES = 60 + @dataclass(slots=True) class Cotation: @@ -30,12 +57,24 @@ class Cotation: pertinence: Pertinence | None score: int expressions: list[str] + exclusions: list[str] = field(default_factory=list) @property def note(self) -> str | None: + if self.exclusions and not self.expressions: + return ( + "Le rapport écarte explicitement ce territoire : " + + ", ".join(f"« {e} »" for e in self.exclusions[:3]) + ) if not self.expressions: return None - return "Détecté d'après : " + ", ".join(self.expressions[:6]) + + note = "Détecté d'après : " + ", ".join(self.expressions[:6]) + if self.exclusions: + note += " — mais le rapport écarte par ailleurs : " + ", ".join( + f"« {e} »" for e in self.exclusions[:2] + ) + return note @functools.lru_cache(maxsize=1) @@ -60,18 +99,31 @@ def _motifs() -> list[tuple[re.Pattern[str], str, int]]: def coter(*fragments: str | None) -> Cotation: - """Cote la pertinence guadeloupéenne d'un ensemble de fragments de texte.""" + """Cote la pertinence guadeloupéenne d'un ensemble de fragments de texte. + + Une mention niée ne compte pas. « Sans portée pour la Guadeloupe » nomme le + territoire pour l'écarter : la lire comme une pertinence forte inverserait + le sens de la source. + """ plat = sans_accents(" ".join(f for f in fragments if f)).lower() if not plat.strip(): return Cotation(pertinence=None, score=0, expressions=[]) score = 0 trouvees: list[str] = [] + exclusions: list[str] = [] for motif, expression, poids in _motifs(): - if motif.search(plat): + positions = [m.start() for m in motif.finditer(plat)] + if not positions: + continue + + retenues = [p for p in positions if not _est_niee(plat, p)] + if retenues: score += poids trouvees.append(expression) + else: + exclusions.append(_extrait_autour(plat, positions[0])) seuils = _configuration()["guadeloupe"]["seuils"] if score >= seuils["forte"]: @@ -80,10 +132,28 @@ def coter(*fragments: str | None) -> Cotation: pertinence = Pertinence.MOYENNE elif score >= seuils["faible"]: pertinence = Pertinence.FAIBLE + elif exclusions: + # Le territoire est nommé, mais pour être écarté : la pertinence est + # faible, et le motif dit pourquoi. + pertinence = Pertinence.FAIBLE else: pertinence = None - return Cotation(pertinence=pertinence, score=score, expressions=trouvees) + return Cotation( + pertinence=pertinence, score=score, expressions=trouvees, exclusions=exclusions + ) + + +def _est_niee(plat: str, position: int) -> bool: + """Vrai si une tournure d'exclusion entoure la mention trouvée.""" + voisinage = plat[max(0, position - _AVANT) : position + _APRES] + return any(negation in voisinage for negation in NEGATIONS) + + +def _extrait_autour(plat: str, position: int, largeur: int = 70) -> str: + debut = max(0, position - largeur) + fin = min(len(plat), position + largeur) + return " ".join(plat[debut:fin].split()) MOTIF_VERIFICATION = ( diff --git a/pipeline/migrations/005_vue_rowid.sql b/pipeline/migrations/005_vue_rowid.sql new file mode 100644 index 0000000..e53652d --- /dev/null +++ b/pipeline/migrations/005_vue_rowid.sql @@ -0,0 +1,34 @@ +-- ============================================================================ +-- Migration 005 — exposer le rowid dans la vue de consultation +-- +-- L'index FTS5 est à contenu externe : il se joint à la table `textes` par +-- `rowid`. Or une vue SQLite n'a pas de rowid propre, et `v_textes.rowid` +-- échoue avec « no such column ». +-- +-- Sans cette colonne, l'application ne peut pas à la fois filtrer sur les +-- champs dérivés de la vue (`devant_cc`) et joindre l'index plein texte — soit +-- exactement ce que demande la recherche à facettes. +-- ============================================================================ + +DROP VIEW v_textes; + +CREATE VIEW v_textes AS +SELECT + t.rowid AS rowid, + t.*, + (SELECT COUNT(*) FROM sources s WHERE s.texte_id = t.id) AS nb_sources, + (SELECT COUNT(*) FROM evenements e WHERE e.texte_id = t.id) AS nb_evenements, + CASE + WHEN t.statut = 'saisie_cc' THEN 1 + WHEN EXISTS (SELECT 1 FROM decisions_cc d + WHERE d.texte_id = t.id + AND d.date_decision IS NULL + AND COALESCE(d.resultat, 'en_instance') = 'en_instance') THEN 1 + WHEN EXISTS (SELECT 1 FROM evenements e + WHERE e.texte_id = t.id AND e.type_etape = 'saisine_cc') + AND NOT EXISTS (SELECT 1 FROM evenements e + WHERE e.texte_id = t.id AND e.type_etape = 'decision_cc') + AND t.statut <> 'promulguee' THEN 1 + ELSE 0 + END AS devant_cc +FROM textes t; diff --git a/scripts/verifier-conformite.sh b/scripts/verifier-conformite.sh new file mode 100644 index 0000000..6b736ed --- /dev/null +++ b/scripts/verifier-conformite.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Contrôles de conformité au cahier des charges (§3bis et §8). +# +# Ces vérifications sont mécaniques : elles constatent, elles n'interprètent +# pas. Un seul contrôle en échec fait échouer la commande. +# ───────────────────────────────────────────────────────────────────────────── +set -uo pipefail +cd "$(dirname "$0")/.." + +echecs=0 +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +ko() { + printf ' \033[31m✗\033[0m %s\n' "$1" + echecs=$((echecs + 1)) +} +titre() { printf '\n\033[1m%s\033[0m\n' "$1"; } + +titre "§3bis — architecture de rendu" + +# Seule une déclaration exportée est lue par SvelteKit : chercher la chaîne +# brute ferait échouer le contrôle sur les commentaires qui la mentionnent, +# y compris ceux qui expliquent pourquoi on ne la pose pas. +MOTIF_SSR='^[[:space:]]*export[[:space:]]+const[[:space:]]+ssr[[:space:]]*=[[:space:]]*false' +MOTIF_CSR='^[[:space:]]*export[[:space:]]+const[[:space:]]+csr[[:space:]]*=[[:space:]]*false' + +if grep -rnE "$MOTIF_SSR" web/src --include='*.ts' --include='*.js' --include='*.svelte' -q; then + ko "un fichier déclare export const ssr = false (le mode SPA est écarté)" + grep -rnE "$MOTIF_SSR" web/src --include='*.ts' --include='*.js' --include='*.svelte' +else + ok "aucun export const ssr = false : le rendu serveur est le défaut partout" +fi + +if grep -rnE "$MOTIF_CSR" web/src/routes/+layout.ts web/src/routes/+layout.server.ts 2>/dev/null -q; then + ko "csr = false déclaré au layout racine" +else + ok "aucun csr = false global" +fi + +for page in a-propos methode; do + if grep -q "prerender = true" "web/src/routes/$page/+page.ts" 2>/dev/null; then + ok "prerender = true sur /$page" + else + ko "prerender = true absent de /$page" + fi +done + +grep -q "adapter-node" web/svelte.config.js && + ok "adapter-node en place" || ko "adapter-node absent de svelte.config.js" + +grep -q "precompress: true" web/svelte.config.js && + ok "precompress activé" || ko "precompress non activé" + +titre "§3 — souveraineté des ressources" + +tiers=$(grep -rnE "https?://(fonts\.googleapis|fonts\.gstatic|cdn\.|unpkg|jsdelivr|googletagmanager|google-analytics)" \ + web/src web/static 2>/dev/null | grep -v Binary || true) +if [ -z "$tiers" ]; then + ok "aucune ressource tierce référencée (polices, CDN, traceurs)" +else + ko "ressources tierces trouvées :" + echo "$tiers" +fi + +nb_polices=$(find web/static/polices -name '*.woff2' 2>/dev/null | wc -l) +[ "$nb_polices" -gt 0 ] && + ok "polices auto-hébergées ($nb_polices fichiers woff2)" || + ko "polices non auto-hébergées — lancer npm run polices" + +titre "§8 — livrables" + +for chemin in pipeline web data systemd tests README.md README-pipeline.md .env.example Makefile; do + [ -e "$chemin" ] && ok "présent : $chemin" || ko "manquant : $chemin" +done + +[ -f web/build/index.js ] && + ok "build de production présent" || ko "build absent — lancer make build" + +titre "§8.2 — intégrité des données" + +if [ -f data/veille.db ]; then + total=$(sqlite3 data/veille.db "SELECT COUNT(*) FROM textes") + orphelins=$(sqlite3 data/veille.db \ + "SELECT COUNT(*) FROM textes t WHERE NOT EXISTS (SELECT 1 FROM sources s WHERE s.texte_id = t.id)") + cc=$(sqlite3 data/veille.db "SELECT COUNT(*) FROM v_textes WHERE devant_cc = 1") + sans_url=$(sqlite3 data/veille.db \ + "SELECT COUNT(*) FROM sources WHERE url NOT LIKE 'http%'") + + [ "$total" -ge 49 ] && ok "$total textes (≥ 49 exigés)" || ko "$total textes, 49 exigés" + [ "$orphelins" -eq 0 ] && ok "aucun texte sans source URL" || ko "$orphelins textes sans source" + [ "$sans_url" -eq 0 ] && ok "toutes les sources ont une URL absolue" || ko "$sans_url sources sans URL" + [ "$cc" -ge 7 ] && ok "$cc textes devant le Conseil constitutionnel (≥ 7)" || ko "$cc textes, 7 attendus" +else + ko "base absente — lancer make seed" +fi + +titre "Résultat" +if [ "$echecs" -eq 0 ]; then + printf ' \033[32mTous les contrôles passent.\033[0m\n\n' +else + printf ' \033[31m%d contrôle(s) en échec.\033[0m\n\n' "$echecs" + exit 1 +fi diff --git a/systemd/veille-legislative.service b/systemd/veille-legislative.service new file mode 100644 index 0000000..bf54e10 --- /dev/null +++ b/systemd/veille-legislative.service @@ -0,0 +1,47 @@ +# Passage de veille : collecte les sources officielles et met la base à jour. +# +# Unité oneshot déclenchée par veille-legislative.timer. Installation : +# sudo cp systemd/veille-*.{service,timer,socket} /etc/systemd/system/ +# sudo systemctl daemon-reload +# sudo systemctl enable --now veille-legislative.timer + +[Unit] +Description=Veille législative — passage de collecte +Documentation=file:///opt/veille-legislative/README-pipeline.md +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +User=veille +Group=veille +WorkingDirectory=/opt/veille-legislative +Environment=PYTHONUNBUFFERED=1 +EnvironmentFile=/opt/veille-legislative/.env +ExecStart=/opt/veille-legislative/.venv/bin/python -m pipeline.update + +# Un passage qui s'éternise a rencontré un problème réseau : on l'arrête. +TimeoutStartSec=900 + +# Durcissement : le pipeline lit le réseau et écrit un seul fichier. +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/veille-legislative/data +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=true +LockPersonality=true +MemoryDenyWriteExecute=true +SystemCallFilter=@system-service +SystemCallErrorNumber=EPERM + +StandardOutput=journal +StandardError=journal +SyslogIdentifier=veille-legislative + +[Install] +WantedBy=multi-user.target diff --git a/systemd/veille-legislative.timer b/systemd/veille-legislative.timer new file mode 100644 index 0000000..110c348 --- /dev/null +++ b/systemd/veille-legislative.timer @@ -0,0 +1,26 @@ +# Cadence de la veille : deux fois par jour, plus un passage renforcé le +# dimanche pour rattraper les publications de fin de semaine au Journal officiel. +# +# Les décisions du Conseil constitutionnel tombent en semaine, les promulgations +# suivent de un à trois jours : deux passages quotidiens suffisent largement, et +# ménagent des services publics gratuits. + +[Unit] +Description=Déclenchement de la veille législative (6 h, 18 h, et dimanche 9 h) +Documentation=file:///opt/veille-legislative/README-pipeline.md + +[Timer] +OnCalendar=*-*-* 06:00:00 +OnCalendar=*-*-* 18:00:00 +OnCalendar=Sun *-*-* 09:00:00 + +# Dispersion : inutile de frapper Légifrance à la seconde ronde en même temps +# que tous les autres services qui l'interrogent. +RandomizedDelaySec=900 + +# Rattrape le passage manqué si la machine était éteinte. +Persistent=true +Unit=veille-legislative.service + +[Install] +WantedBy=timers.target diff --git a/systemd/veille-web.service b/systemd/veille-web.service new file mode 100644 index 0000000..2b35978 --- /dev/null +++ b/systemd/veille-web.service @@ -0,0 +1,52 @@ +# Application web de la veille législative (SvelteKit, adapter-node). +# +# Démarrée par veille-web.socket à la première requête, elle s'arrête d'elle-même +# après IDLE_TIMEOUT secondes sans trafic, et systemd la réveille à la suivante. +# +# Node ≥ 20.6 requis : les variables ne sont PAS chargées automatiquement en +# production, d'où --env-file. + +[Unit] +Description=Veille législative — application web +Documentation=file:///opt/veille-legislative/README.md +Requires=veille-web.socket +After=network.target veille-web.socket + +[Service] +Type=simple +User=veille +Group=veille +WorkingDirectory=/opt/veille-legislative/web +ExecStart=/usr/bin/node --env-file=/opt/veille-legislative/.env build + +Environment=NODE_ENV=production +Environment=VEILLE_DB=/opt/veille-legislative/data/veille.db +# Mise en sommeil après cinq minutes sans requête. +Environment=IDLE_TIMEOUT=300 +# Obligatoire derrière un reverse proxy, sinon les soumissions de formulaire +# sont refusées (« Cross-site POST form submissions are forbidden »). +# Alternative : PROTOCOL_HEADER=x-forwarded-proto + HOST_HEADER=x-forwarded-host +Environment=ORIGIN=https://veille.example.org + +Restart=on-failure +RestartSec=5 + +# Durcissement : l'application ne fait que lire la base. +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadOnlyPaths=/opt/veille-legislative/data +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=true +LockPersonality=true + +StandardOutput=journal +StandardError=journal +SyslogIdentifier=veille-web + +[Install] +WantedBy=multi-user.target diff --git a/systemd/veille-web.socket b/systemd/veille-web.socket new file mode 100644 index 0000000..4c96684 --- /dev/null +++ b/systemd/veille-web.socket @@ -0,0 +1,17 @@ +# Activation par socket de l'application web. +# +# systemd tient le port ouvert et ne démarre l'application qu'à la première +# requête. Combinée à IDLE_TIMEOUT dans le service, l'application se met en +# sommeil sans trafic : l'empreinte mémoire d'un site de veille peu fréquenté +# tombe à zéro entre deux visites, ce qui compte sur un VPS à 5 €. + +[Unit] +Description=Socket d'écoute de la veille législative + +[Socket] +ListenStream=127.0.0.1:3971 +# Le service hérite du descripteur : adapter-node lit LISTEN_PID et LISTEN_FDS. +Accept=no + +[Install] +WantedBy=sockets.target diff --git a/tests/test_update.py b/tests/test_update.py index 72712c9..2590cda 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -309,3 +309,58 @@ def test_rapport_lisible(base) -> None: texte = rapport.en_texte() assert "Rapport de veille" in texte assert "conforme aux sources officielles" in texte + + +# ───────────────────────────────────────────────────────────────────────────── +# Cotation Guadeloupe : les négations +# ───────────────────────────────────────────────────────────────────────────── + + +def test_une_mention_niee_ne_vaut_pas_pertinence() -> None: + """« Sans portée pour la Guadeloupe » nomme le territoire pour l'écarter. + + Sans lecture de la négation, la PPL montagne recevait la cotation la plus + forte alors que le rapport dit l'inverse — et remontait dans la facette + « pertinence forte pour la Guadeloupe ». + """ + from pipeline.guadeloupe import coter + + cotation = coter( + "PPL « pour une montagne vivante et souveraine ». Continuité de " + "l'urbanisation ; sans portée pour la Guadeloupe." + ) + assert cotation.pertinence is Pertinence.FAIBLE + assert cotation.score == 0 + assert cotation.exclusions + assert "écarte explicitement" in (cotation.note or "") + + +def test_pas_de_volet_outre_mer_est_une_negation() -> None: + from pipeline.guadeloupe import coter + + cotation = coter("PJL logement. Véto des maires ; pas de volet outre-mer à ce stade.") + assert cotation.pertinence is Pertinence.FAIBLE + assert cotation.score == 0 + + +def test_une_mention_affirmee_compte_toujours() -> None: + from pipeline.guadeloupe import coter + + cotation = coter( + "Chlordécone : responsabilité de l'État. Guadeloupe et Martinique au cœur du texte." + ) + assert cotation.pertinence is Pertinence.FORTE + assert "guadeloupe" in cotation.expressions + assert not cotation.exclusions + + +def test_la_negation_ne_porte_que_sur_son_voisinage() -> None: + """Une exclusion lointaine ne doit pas annuler une mention affirmée.""" + from pipeline.guadeloupe import coter + + cotation = coter( + "Fouilles jusqu'à 40 km du littoral, soit la quasi-totalité de la Guadeloupe. " + + "Texte de portée générale. " * 12 + + "Le dispositif est sans objet pour les collectivités du Pacifique." + ) + assert cotation.pertinence is Pertinence.FORTE diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..85016d2 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.svelte-kit/ +build/ +static/polices/ diff --git a/web/.npmrc b/web/.npmrc new file mode 100644 index 0000000..d6b5390 --- /dev/null +++ b/web/.npmrc @@ -0,0 +1,3 @@ +engine-strict=false +fund=false +audit=false diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..9a841ad --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,2304 @@ +{ + "name": "veille-legislative-971-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "veille-legislative-971-web", + "version": "1.0.0", + "license": "AGPL-3.0-or-later", + "dependencies": { + "better-sqlite3": "11.8.1" + }, + "devDependencies": { + "@fontsource/archivo": "5.1.0", + "@fontsource/inter": "5.1.0", + "@sveltejs/adapter-node": "5.2.12", + "@sveltejs/kit": "2.17.2", + "@sveltejs/vite-plugin-svelte": "5.0.3", + "@types/better-sqlite3": "7.6.12", + "svelte": "5.20.2", + "svelte-check": "4.1.4", + "typescript": "5.7.3", + "vite": "6.1.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/archivo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource/archivo/-/archivo-5.1.0.tgz", + "integrity": "sha512-zusUDuLRWmNYi5DhaXado+uj9ip8IarqgUdUlXQOrEVVQbN1gpueWy0Qbxnn6DSfEXo2cNneMPlv7MtG+XU8Zg==", + "dev": true, + "license": "OFL-1.1" + }, + "node_modules/@fontsource/inter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.1.0.tgz", + "integrity": "sha512-zKZR3kf1G0noIes1frLfOHP5EXVVm0M7sV/l9f/AaYf+M/DId35FO4LkigWjqWYjTJZGgplhdv4cB+ssvCqr5A==", + "dev": true, + "license": "OFL-1.1" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.9", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz", + "integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", + "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.17.2.tgz", + "integrity": "sha512-Vypk02baf7qd3SOB1uUwUC/3Oka+srPo2J0a8YN3EfJypRshDkNx9HzNKjSmhOnGWwT+SSO06+N0mAb8iVTmTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "import-meta-resolve": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz", + "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.0", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.15", + "vitefu": "^1.0.4" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.12", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", + "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-typescript": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", + "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": ">=8.9.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz", + "integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", + "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.9.tgz", + "integrity": "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.20.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.20.2.tgz", + "integrity": "sha512-aYXJreNUiyTob0QOzRZeBXZMGeFZDch6SrSRV8QTncZb6zj0O3BEdUzPpojuHQ1pTvk+KX7I6rZCXPUf8pTPxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "acorn-typescript": "^1.4.13", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.3", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.4.tgz", + "integrity": "sha512-v0j7yLbT29MezzaQJPEDwksybTE2Ups9rUxEXy92T06TiA0cbqcO8wAOwNUVkFW6B0hsYHA+oAX3BS8b/2oHtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", + "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.5.1", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..21f67f6 --- /dev/null +++ b/web/package.json @@ -0,0 +1,32 @@ +{ + "name": "veille-legislative-971-web", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "Interface de la veille législative Guadeloupe — SvelteKit, rendu serveur", + "license": "AGPL-3.0-or-later", + "scripts": { + "dev": "vite dev --port 5971", + "build": "vite build", + "preview": "vite preview", + "start": "node build", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "polices": "node scripts/copier-polices.js" + }, + "devDependencies": { + "@fontsource/archivo": "5.1.0", + "@fontsource/inter": "5.1.0", + "@sveltejs/adapter-node": "5.2.12", + "@sveltejs/kit": "2.17.2", + "@sveltejs/vite-plugin-svelte": "5.0.3", + "@types/better-sqlite3": "7.6.12", + "svelte": "5.20.2", + "svelte-check": "4.1.4", + "typescript": "5.7.3", + "vite": "6.1.0" + }, + "dependencies": { + "better-sqlite3": "11.8.1" + } +} diff --git a/web/scripts/copier-polices.js b/web/scripts/copier-polices.js new file mode 100644 index 0000000..02dd5ed --- /dev/null +++ b/web/scripts/copier-polices.js @@ -0,0 +1,33 @@ +/** + * Copie les polices Archivo et Inter depuis node_modules vers static/polices/. + * + * Le self-hébergement n'est pas une préférence : la politique de sécurité de + * contenu déclarée dans svelte.config.js interdit toute requête vers un hôte + * tiers, Google Fonts compris. + */ +import { copyFileSync, mkdirSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const DESTINATION = 'static/polices'; +mkdirSync(DESTINATION, { recursive: true }); + +const familles = [ + { paquet: '@fontsource/archivo', graisses: ['400', '600', '700', '900'] }, + { paquet: '@fontsource/inter', graisses: ['400', '500', '600', '700'] } +]; + +let copiees = 0; +for (const { paquet, graisses } of familles) { + const source = join('node_modules', paquet, 'files'); + for (const fichier of readdirSync(source)) { + const estLatin = fichier.includes('-latin-') && fichier.endsWith('.woff2'); + const estNormal = fichier.includes('-normal.woff2'); + const graisseVoulue = graisses.some((g) => fichier.includes(`-${g}-`)); + if (estLatin && estNormal && graisseVoulue) { + copyFileSync(join(source, fichier), join(DESTINATION, fichier)); + copiees += 1; + } + } +} + +console.log(`${copiees} fichiers de police copiés dans ${DESTINATION}`); diff --git a/web/src/app.html b/web/src/app.html new file mode 100644 index 0000000..fe56608 --- /dev/null +++ b/web/src/app.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/web/src/lib/composants/BadgeGuadeloupe.svelte b/web/src/lib/composants/BadgeGuadeloupe.svelte new file mode 100644 index 0000000..f0adbc7 --- /dev/null +++ b/web/src/lib/composants/BadgeGuadeloupe.svelte @@ -0,0 +1,48 @@ + + +{#if pertinence} + + {LIBELLES[pertinence] ?? pertinence}{#if aVerifier}*{/if} + +{/if} + + diff --git a/web/src/lib/composants/BadgeStatut.svelte b/web/src/lib/composants/BadgeStatut.svelte new file mode 100644 index 0000000..07be14c --- /dev/null +++ b/web/src/lib/composants/BadgeStatut.svelte @@ -0,0 +1,71 @@ + + + + + {libelle} + +{#if devantCc && statut !== 'saisie_cc'} + + + Devant le Conseil constitutionnel + +{/if} + + diff --git a/web/src/lib/composants/CarteTexte.svelte b/web/src/lib/composants/CarteTexte.svelte new file mode 100644 index 0000000..99b13b5 --- /dev/null +++ b/web/src/lib/composants/CarteTexte.svelte @@ -0,0 +1,129 @@ + + +
+

+ + {#if texte.numero_officiel}n° {texte.numero_officiel}{/if} + {texte.titre_court} + +

+ +
+ + + {LIBELLES_TYPE[texte.type] ?? texte.type} +
+ + {#if libelleDate}

{libelleDate}

{/if} + + {#if texte.extrait} +

{@html texte.extrait}

+ {:else if texte.resume} +

{extrait(texte.resume, 200)}

+ {:else if texte.points_cles.length} +

{extrait(texte.points_cles[0], 200)}

+ {/if} + + {#if texte.themes.length} +
+ {#each texte.themes as theme (theme)} + {LIBELLES_THEME[theme] ?? theme} + {/each} +
+ {/if} + +

+ {texte.nb_sources} + {texte.nb_sources > 1 ? 'sources' : 'source'} + {#if texte.a_verifier === 1} + · à vérifier + {/if} +

+
+ + diff --git a/web/src/lib/composants/PanneauFacettes.svelte b/web/src/lib/composants/PanneauFacettes.svelte new file mode 100644 index 0000000..758f6f2 --- /dev/null +++ b/web/src/lib/composants/PanneauFacettes.svelte @@ -0,0 +1,159 @@ + + + + + diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts new file mode 100644 index 0000000..9948203 --- /dev/null +++ b/web/src/lib/format.ts @@ -0,0 +1,47 @@ +/** Formatage des dates et des libellés, en français. */ + +const MOIS = [ + 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', + 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre' +]; + +/** « 2026-06-12 » → « 12 juin 2026 ». */ +export function dateLongue(iso: string | null | undefined): string { + if (!iso) return ''; + const [a, m, j] = iso.slice(0, 10).split('-').map(Number); + if (!a || !m || !j) return iso; + return `${j === 1 ? '1ᵉʳ' : j} ${MOIS[m - 1]} ${a}`; +} + +/** « 2026-06-12 » → « 12 juin ». */ +export function dateCourte(iso: string | null | undefined): string { + if (!iso) return ''; + const [, m, j] = iso.slice(0, 10).split('-').map(Number); + if (!m || !j) return iso; + return `${j === 1 ? '1ᵉʳ' : j} ${MOIS[m - 1]}`; +} + +export function mois(iso: string): string { + const [a, m] = iso.slice(0, 10).split('-').map(Number); + return `${MOIS[m - 1]} ${a}`; +} + +/** Nombre de jours entre deux dates ISO, positif dans le futur. */ +export function joursRestants(iso: string, depuis = '2026-07-25'): number { + const cible = new Date(iso.slice(0, 10)).getTime(); + const base = new Date(depuis).getTime(); + return Math.round((cible - base) / 86_400_000); +} + +export function pluriel(n: number, singulier: string, plurielMot?: string): string { + return n > 1 ? (plurielMot ?? `${singulier}s`) : singulier; +} + +/** Tronque sur une frontière de mot, sans couper au milieu. */ +export function extrait(texte: string | null | undefined, taille = 180): string { + if (!texte) return ''; + if (texte.length <= taille) return texte; + const coupe = texte.slice(0, taille); + const espace = coupe.lastIndexOf(' '); + return `${coupe.slice(0, espace > 0 ? espace : taille)}…`; +} diff --git a/web/src/lib/libelles.ts b/web/src/lib/libelles.ts new file mode 100644 index 0000000..bdc555d --- /dev/null +++ b/web/src/lib/libelles.ts @@ -0,0 +1,94 @@ +/** + * Libellés d'affichage du vocabulaire métier. + * + * Ce module est partagé serveur et client : il ne touche ni à la base ni au + * système de fichiers. Les composants d'interface en ont besoin, et SvelteKit + * interdit — à raison — d'importer `$lib/server` depuis du code client. + */ + +export const LIBELLES_STATUT: Record = { + promulguee: 'Promulguée', + adoptee_non_promulguee: 'Adoptée, non promulguée', + saisie_cc: 'Devant le Conseil constitutionnel', + validee_cc: 'Validée, en attente de promulgation', + censuree_partiellement: 'Censurée partiellement', + navette: 'En navette', + deposee_non_examinee: 'Déposée, non examinée', + annoncee: 'Annoncée', + rejetee: 'Rejetée' +}; + +export const LIBELLES_THEME: Record = { + justice: 'Justice', + securite: 'Sécurité', + numerique: 'Numérique', + social: 'Social', + fiscal: 'Fiscal', + environnement: 'Environnement', + agriculture: 'Agriculture', + sante: 'Santé', + memoire_patrimoine: 'Mémoire et patrimoine', + outre_mer: 'Outre-mer', + economie: 'Économie', + migration: 'Migration', + institutions: 'Institutions' +}; + +export const LIBELLES_TYPE: Record = { + loi: 'Loi', + loi_organique: 'Loi organique', + pjl: 'Projet de loi', + ppl: 'Proposition de loi', + ordonnance: 'Ordonnance', + decision_cc: 'Décision du Conseil constitutionnel', + decret: 'Décret', + accord_international: 'Accord international' +}; + +export const LIBELLES_PERTINENCE: Record = { + forte: 'Forte', + moyenne: 'Moyenne', + faible: 'Faible' +}; + +export const LIBELLES_CONFIANCE: Record = { + high: 'Élevée', + medium: 'Moyenne', + low: 'Faible' +}; + +export const LIBELLES_IMPACT: Record = { + 'individus:positif': 'Individus — droits étendus', + 'individus:negatif': 'Individus — droits restreints', + 'individus:mixte': 'Individus — effet mixte', + 'associations:positif': 'Associations — droits étendus', + 'associations:negatif': 'Associations — droits restreints', + 'associations:mixte': 'Associations — effet mixte', + 'entreprises:positif': 'Entreprises — obligations allégées', + 'entreprises:negatif': 'Entreprises — obligations renforcées', + 'entreprises:mixte': 'Entreprises — effet mixte' +}; + +export const LIBELLES_ETAPE: Record = { + depot: 'Dépôt', + adoption_1re_lecture: 'Adoption en première lecture', + adoption_definitive: 'Adoption définitive', + commission_mixte_paritaire: 'Commission mixte paritaire', + transmission: 'Transmission', + saisine_cc: 'Saisine du Conseil constitutionnel', + decision_cc: 'Décision du Conseil constitutionnel', + promulgation: 'Promulgation', + publication_jo: 'Publication au Journal officiel', + entree_vigueur: 'Entrée en vigueur', + rejet: 'Rejet', + annonce: 'Annonce', + autre: 'Étape' +}; + +export const LIBELLES_RESULTAT_CC: Record = { + conforme: 'Conforme', + conforme_avec_reserves: 'Conforme avec réserves', + non_conformite_partielle: 'Non-conformité partielle', + non_conformite_totale: 'Non-conformité totale', + en_instance: 'En instance' +}; diff --git a/web/src/lib/server/db.ts b/web/src/lib/server/db.ts new file mode 100644 index 0000000..88b553d --- /dev/null +++ b/web/src/lib/server/db.ts @@ -0,0 +1,53 @@ +/** + * Connexion SQLite partagée. + * + * La base est ouverte une seule fois, en lecture seule : l'application web ne + * modifie jamais les données, c'est le rôle du pipeline. Le mode WAL permet au + * pipeline d'écrire pendant que l'application lit, sans blocage. + */ +import Database from 'better-sqlite3'; +import { existsSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ICI = dirname(fileURLToPath(import.meta.url)); + +/** Emplacements possibles de la base, du plus explicite au plus conventionnel. */ +function trouverLaBase(): string { + const candidats = [ + process.env.VEILLE_DB, + resolve(process.cwd(), '../data/veille.db'), + resolve(process.cwd(), 'data/veille.db'), + resolve(ICI, '../../../../data/veille.db') + ].filter(Boolean) as string[]; + + for (const candidat of candidats) { + if (existsSync(candidat)) return candidat; + } + + throw new Error( + `Base introuvable. Essayés : ${candidats.join(', ')}. ` + + 'Lancer `make seed` à la racine du dépôt, ou définir VEILLE_DB.' + ); +} + +let instance: Database.Database | null = null; + +export function base(): Database.Database { + if (instance) return instance; + + instance = new Database(trouverLaBase(), { readonly: true, fileMustExist: true }); + instance.pragma('journal_mode = WAL'); + // Les requêtes de facettes lisent beaucoup et écrivent rien : un cache + // généreux évite de relire les mêmes pages à chaque requête HTTP. + instance.pragma('cache_size = -16000'); + return instance; +} + +export function fermer(): void { + instance?.close(); + instance = null; +} + +/** Date d'arrêté du corpus de recherche, affichée partout où c'est utile. */ +export const DATE_ARRETE_CORPUS = '2026-07-25'; diff --git a/web/src/lib/server/requetes.ts b/web/src/lib/server/requetes.ts new file mode 100644 index 0000000..071f270 --- /dev/null +++ b/web/src/lib/server/requetes.ts @@ -0,0 +1,573 @@ +/** + * Requêtes de consultation : recherche plein texte, facettes, fiches, calendrier. + * + * Tout se joue en SQL. La recherche plein texte passe par l'index FTS5 à + * tokenizer `unicode61 remove_diacritics 2` — « chlordecone » retrouve + * « chlordécone » — et le classement par `bm25()` pondère les colonnes : un mot + * dans le titre pèse dix fois plus que dans les points clés. + * + * Les facettes sont calculées sur le résultat filtré **par les autres + * facettes**, pas sur la table entière : cocher « santé » doit recompter les + * statuts disponibles parmi les textes de santé, sinon les compteurs mentent. + */ +import { base } from '$serveur/db'; +import { + LIBELLES_CONFIANCE, + LIBELLES_IMPACT, + LIBELLES_PERTINENCE, + LIBELLES_STATUT, + LIBELLES_THEME, + LIBELLES_TYPE +} from '$lib/libelles'; +import type { + Echeance, + Facette, + Filtres, + Insight, + Resultats, + Texte, + TexteDetaille +} from '$lib/types'; + +const PAR_PAGE = 20; + +/** Pondération bm25 : titre court, titre officiel, résumé, points clés, note, numéro. */ +const POIDS_BM25 = '10.0, 5.0, 3.0, 2.0, 2.0, 1.0'; + +// ───────────────────────────────────────────────────────────────────────────── +// Lecture des filtres depuis l'URL +// ───────────────────────────────────────────────────────────────────────────── +export function lireFiltres(parametres: URLSearchParams): Filtres { + const multiple = (nom: string) => + parametres + .getAll(nom) + .flatMap((valeur) => valeur.split(',')) + .map((valeur) => valeur.trim()) + .filter(Boolean); + + const page = Number.parseInt(parametres.get('page') ?? '1', 10); + + return { + q: (parametres.get('q') ?? '').trim(), + statut: multiple('statut'), + theme: multiple('theme'), + type: multiple('type'), + impact: multiple('impact'), + guadeloupe: multiple('guadeloupe'), + confiance: multiple('confiance'), + from: parametres.get('from'), + to: parametres.get('to'), + tri: parametres.get('tri') === 'date' ? 'date' : 'pertinence', + page: Number.isFinite(page) && page > 0 ? page : 1 + }; +} + +/** Reconstruit une URL de recherche en basculant une valeur de facette. */ +export function basculerFacette( + filtres: Filtres, + champ: keyof Filtres, + valeur: string +): string { + const parametres = new URLSearchParams(); + if (filtres.q) parametres.set('q', filtres.q); + + for (const nom of ['statut', 'theme', 'type', 'impact', 'guadeloupe', 'confiance'] as const) { + let valeurs = [...filtres[nom]]; + if (nom === champ) { + valeurs = valeurs.includes(valeur) + ? valeurs.filter((v) => v !== valeur) + : [...valeurs, valeur]; + } + for (const v of valeurs) parametres.append(nom, v); + } + + if (filtres.from) parametres.set('from', filtres.from); + if (filtres.to) parametres.set('to', filtres.to); + if (filtres.tri !== 'pertinence') parametres.set('tri', filtres.tri); + + const chaine = parametres.toString(); + return chaine ? `/recherche?${chaine}` : '/recherche'; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Construction du SQL +// ───────────────────────────────────────────────────────────────────────────── +interface Clause { + sql: string; + parametres: unknown[]; +} + +/** + * Conditions issues des filtres, hors recherche plein texte. + * + * `exclure` permet de recalculer les facettes d'un champ en ignorant ses + * propres sélections — c'est ce qui rend les compteurs honnêtes. + */ +function conditions(filtres: Filtres, exclure?: string): Clause { + const morceaux: string[] = []; + const parametres: unknown[] = []; + + if (filtres.statut.length && exclure !== 'statut') { + // `saisie_cc` n'est pas seulement un statut : c'est aussi l'état des + // textes dont une affaire est en instance devant le Conseil. La vue + // `v_textes` expose cette lecture combinée sous `devant_cc`. + const statutsSimples = filtres.statut.filter((s) => s !== 'saisie_cc'); + const clauses: string[] = []; + if (statutsSimples.length) { + clauses.push(`statut IN (${statutsSimples.map(() => '?').join(', ')})`); + parametres.push(...statutsSimples); + } + if (filtres.statut.includes('saisie_cc')) clauses.push('devant_cc = 1'); + morceaux.push(`(${clauses.join(' OR ')})`); + } + + if (filtres.theme.length && exclure !== 'theme') { + const clauses = filtres.theme.map( + () => `EXISTS (SELECT 1 FROM json_each(themes) WHERE json_each.value = ?)` + ); + morceaux.push(`(${clauses.join(' OR ')})`); + parametres.push(...filtres.theme); + } + + if (filtres.type.length && exclure !== 'type') { + morceaux.push(`type IN (${filtres.type.map(() => '?').join(', ')})`); + parametres.push(...filtres.type); + } + + if (filtres.guadeloupe.length && exclure !== 'guadeloupe') { + morceaux.push( + `guadeloupe_pertinence IN (${filtres.guadeloupe.map(() => '?').join(', ')})` + ); + parametres.push(...filtres.guadeloupe); + } + + if (filtres.confiance.length && exclure !== 'confiance') { + morceaux.push(`confiance IN (${filtres.confiance.map(() => '?').join(', ')})`); + parametres.push(...filtres.confiance); + } + + if (filtres.impact.length && exclure !== 'impact') { + // Un filtre d'impact s'écrit « public:sens », par exemple + // « associations:negatif ». Les deux moitiés doivent correspondre + // ensemble, d'où l'accès direct par clé JSON. + const clauses = filtres.impact.map(() => `json_extract(impacts, ?) = ?`); + morceaux.push(`(${clauses.join(' OR ')})`); + for (const valeur of filtres.impact) { + const [pub, sens] = valeur.split(':'); + parametres.push(`$.${pub}.sens`, sens); + } + } + + if (filtres.from) { + morceaux.push(`COALESCE(date_promulgation, date_adoption, statut_date) >= ?`); + parametres.push(filtres.from); + } + if (filtres.to) { + morceaux.push(`COALESCE(date_promulgation, date_adoption, statut_date) <= ?`); + parametres.push(filtres.to); + } + + return { sql: morceaux.length ? morceaux.join(' AND ') : '1 = 1', parametres }; +} + +/** + * Traduit une saisie libre en requête FTS5. + * + * Chaque terme est cité pour neutraliser la syntaxe FTS (`NEAR`, `*`, `:`) + * qu'un visiteur ne saisit pas volontairement, et suffixé d'une étoile pour + * accepter les préfixes : « chlord » trouve « chlordécone ». + */ +function requeteFts(saisie: string): string { + const termes = saisie + .replace(/["^*:()]/g, ' ') + .split(/\s+/) + .map((terme) => terme.trim()) + .filter((terme) => terme.length > 1); + + if (!termes.length) return ''; + return termes.map((terme) => `"${terme}"*`).join(' AND '); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Recherche +// ───────────────────────────────────────────────────────────────────────────── +export function rechercher(filtres: Filtres): Resultats { + const db = base(); + const where = conditions(filtres); + const fts = filtres.q ? requeteFts(filtres.q) : ''; + + const source = fts + ? `v_textes t JOIN textes_fts f ON f.rowid = t.rowid AND textes_fts MATCH ?` + : `v_textes t`; + const parametresSource = fts ? [fts] : []; + + const ordre = fts + ? filtres.tri === 'date' + ? `COALESCE(t.date_promulgation, t.date_adoption, t.statut_date) DESC` + : `bm25(textes_fts, ${POIDS_BM25})` + : `COALESCE(t.date_promulgation, t.date_adoption, t.statut_date) DESC, t.id`; + + const total = ( + db + .prepare(`SELECT COUNT(*) AS n FROM ${source} WHERE ${where.sql}`) + .get(...parametresSource, ...where.parametres) as { n: number } + ).n; + + const extrait = fts + ? `snippet(textes_fts, -1, '', '', '…', 24) AS extrait` + : `NULL AS extrait`; + + const lignes = db + .prepare( + `SELECT t.*, ${extrait} + FROM ${source} + WHERE ${where.sql} + ORDER BY ${ordre} + LIMIT ? OFFSET ?` + ) + .all( + ...parametresSource, + ...where.parametres, + PAR_PAGE, + (filtres.page - 1) * PAR_PAGE + ) as Record[]; + + return { + textes: lignes.map(hydrater), + total, + page: filtres.page, + pages: Math.max(1, Math.ceil(total / PAR_PAGE)), + facettes: { + statut: facetteStatut(filtres, fts), + theme: facetteTheme(filtres, fts), + type: facetteSimple(filtres, fts, 'type', LIBELLES_TYPE), + guadeloupe: facetteSimple( + filtres, + fts, + 'guadeloupe_pertinence', + LIBELLES_PERTINENCE, + 'guadeloupe' + ), + impact: facetteImpact(filtres, fts), + confiance: facetteSimple(filtres, fts, 'confiance', LIBELLES_CONFIANCE) + } + }; +} + +function sourceEtParametres(fts: string): [string, unknown[]] { + return fts + ? [`v_textes t JOIN textes_fts f ON f.rowid = t.rowid AND textes_fts MATCH ?`, [fts]] + : [`v_textes t`, []]; +} + +function facetteSimple( + filtres: Filtres, + fts: string, + colonne: string, + libelles: Record, + champFiltre?: keyof Filtres +): Facette[] { + const champ = (champFiltre ?? colonne) as keyof Filtres; + const where = conditions(filtres, champ as string); + const [source, parametresSource] = sourceEtParametres(fts); + + const lignes = base() + .prepare( + `SELECT ${colonne} AS valeur, COUNT(*) AS nombre + FROM ${source} + WHERE ${where.sql} AND ${colonne} IS NOT NULL + GROUP BY ${colonne} + ORDER BY nombre DESC` + ) + .all(...parametresSource, ...where.parametres) as { valeur: string; nombre: number }[]; + + const selection = filtres[champ] as string[]; + return lignes.map((ligne) => ({ + valeur: ligne.valeur, + libelle: libelles[ligne.valeur] ?? ligne.valeur, + nombre: ligne.nombre, + actif: selection.includes(ligne.valeur) + })); +} + +function facetteStatut(filtres: Filtres, fts: string): Facette[] { + const facettes = facetteSimple(filtres, fts, 'statut', LIBELLES_STATUT); + + // Facette dérivée : « devant le Conseil constitutionnel » recouvre à la + // fois le statut `saisie_cc` et les textes ayant une affaire en instance. + const where = conditions(filtres, 'statut'); + const [source, parametresSource] = sourceEtParametres(fts); + const nombre = ( + base() + .prepare(`SELECT COUNT(*) AS n FROM ${source} WHERE ${where.sql} AND devant_cc = 1`) + .get(...parametresSource, ...where.parametres) as { n: number } + ).n; + + if (nombre > 0 && !facettes.some((f) => f.valeur === 'saisie_cc')) { + facettes.unshift({ + valeur: 'saisie_cc', + libelle: LIBELLES_STATUT.saisie_cc, + nombre, + actif: filtres.statut.includes('saisie_cc') + }); + } + + return facettes; +} + +function facetteTheme(filtres: Filtres, fts: string): Facette[] { + const where = conditions(filtres, 'theme'); + const [source, parametresSource] = sourceEtParametres(fts); + + const lignes = base() + .prepare( + `SELECT json_each.value AS valeur, COUNT(*) AS nombre + FROM ${source}, json_each(t.themes) + WHERE ${where.sql} + GROUP BY json_each.value + ORDER BY nombre DESC` + ) + .all(...parametresSource, ...where.parametres) as { valeur: string; nombre: number }[]; + + return lignes.map((ligne) => ({ + valeur: ligne.valeur, + libelle: LIBELLES_THEME[ligne.valeur] ?? ligne.valeur, + nombre: ligne.nombre, + actif: filtres.theme.includes(ligne.valeur) + })); +} + +function facetteImpact(filtres: Filtres, fts: string): Facette[] { + const where = conditions(filtres, 'impact'); + const [source, parametresSource] = sourceEtParametres(fts); + + const lignes = base() + .prepare( + `SELECT json_each.key || ':' || json_extract(json_each.value, '$.sens') AS valeur, + COUNT(*) AS nombre + FROM ${source}, json_each(t.impacts) + WHERE ${where.sql} + GROUP BY valeur + ORDER BY nombre DESC` + ) + .all(...parametresSource, ...where.parametres) as { valeur: string; nombre: number }[]; + + return lignes + .filter((ligne) => !ligne.valeur.endsWith(':neutre')) + .map((ligne) => ({ + valeur: ligne.valeur, + libelle: LIBELLES_IMPACT[ligne.valeur] ?? ligne.valeur, + nombre: ligne.nombre, + actif: filtres.impact.includes(ligne.valeur) + })); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Fiche détaillée +// ───────────────────────────────────────────────────────────────────────────── +export function texte(slug: string): TexteDetaille | null { + const db = base(); + const ligne = db.prepare(`SELECT * FROM v_textes WHERE id = ?`).get(slug) as + | Record + | undefined; + if (!ligne) return null; + + const detaille = hydrater(ligne) as TexteDetaille; + + detaille.sources = db + .prepare( + `SELECT url, titre, editeur, date_publication, tier, extrait_verbatim, + contexte, confiance, marqueur, fichier_origine + FROM sources WHERE texte_id = ? + ORDER BY tier ASC, date_publication DESC NULLS LAST, id` + ) + .all(slug) as TexteDetaille['sources']; + + detaille.evenements = db + .prepare( + `SELECT date_evenement, type_etape, description, source_url, previsionnel + FROM evenements WHERE texte_id = ? + ORDER BY date_evenement ASC, id` + ) + .all(slug) as TexteDetaille['evenements']; + + detaille.decisions_cc = db + .prepare(`SELECT * FROM decisions_cc WHERE texte_id = ? ORDER BY numero_affaire`) + .all(slug) as TexteDetaille['decisions_cc']; + + return detaille; +} + +export function tousLesSlugs(): string[] { + return (base().prepare(`SELECT id FROM textes ORDER BY id`).all() as { id: string }[]).map( + (l) => l.id + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Tableau de bord +// ───────────────────────────────────────────────────────────────────────────── +export interface Compteurs { + total: number; + parStatut: { valeur: string; libelle: string; nombre: number }[]; + devantCc: number; + guadeloupeForte: number; + aVerifier: number; + sources: number; +} + +export function compteurs(): Compteurs { + const db = base(); + const un = (sql: string) => (db.prepare(sql).get() as { n: number }).n; + + return { + total: un(`SELECT COUNT(*) AS n FROM textes`), + parStatut: ( + db + .prepare( + `SELECT statut AS valeur, COUNT(*) AS nombre FROM textes + GROUP BY statut ORDER BY nombre DESC` + ) + .all() as { valeur: string; nombre: number }[] + ).map((l) => ({ ...l, libelle: LIBELLES_STATUT[l.valeur] ?? l.valeur })), + devantCc: un(`SELECT COUNT(*) AS n FROM v_textes WHERE devant_cc = 1`), + guadeloupeForte: un( + `SELECT COUNT(*) AS n FROM textes WHERE guadeloupe_pertinence = 'forte'` + ), + aVerifier: un(`SELECT COUNT(*) AS n FROM textes WHERE a_verifier = 1`), + sources: un(`SELECT COUNT(*) AS n FROM sources`) + }; +} + +/** Échéances des `jours` prochains jours, textes et repères nationaux mêlés. */ +export function prochainesEcheances(jours = 60, aPartirDe?: string): Echeance[] { + const depart = aPartirDe ?? '2026-07-25'; + const fin = new Date(depart); + fin.setDate(fin.getDate() + jours); + const limite = fin.toISOString().slice(0, 10); + + const db = base(); + + const nationales = db + .prepare( + `SELECT * FROM echeances + WHERE date_echeance >= ? AND date_echeance <= ? + ORDER BY date_echeance` + ) + .all(depart, limite) as Echeance[]; + + const desTextes = db + .prepare( + `SELECT NULL AS id, 'texte-' || id AS code, prochaine_echeance AS date_echeance, + titre_court AS libelle, prochaine_echeance_label AS description, + 'texte' AS portee, id AS texte_id, + CASE WHEN guadeloupe_pertinence = 'forte' THEN 1 ELSE NULL END + AS concerne_guadeloupe, + guadeloupe_note AS note_guadeloupe, 1 AS previsionnel, + NULL AS source_url, NULL AS source_extrait + FROM textes + WHERE prochaine_echeance IS NOT NULL + AND prochaine_echeance >= ? AND prochaine_echeance <= ? + ORDER BY prochaine_echeance` + ) + .all(depart, limite) as Echeance[]; + + return [...nationales, ...desTextes].sort((a, b) => + a.date_echeance.localeCompare(b.date_echeance) + ); +} + +/** Toutes les échéances, pour la page calendrier. */ +export function calendrier(): Echeance[] { + return prochainesEcheances(400, '2026-07-01'); +} + +export function decisionsAttendues(): (Echeance & { numero_affaire: string })[] { + return base() + .prepare( + `SELECT d.numero_affaire, d.date_decision_attendue AS date_echeance, + COALESCE(t.titre_court, d.resume) AS libelle, d.resume AS description, + d.texte_id, t.guadeloupe_pertinence + FROM decisions_cc d + LEFT JOIN textes t ON t.id = d.texte_id + WHERE d.date_decision IS NULL + ORDER BY COALESCE(d.date_decision_attendue, '9999'), d.numero_affaire` + ) + .all() as (Echeance & { numero_affaire: string })[]; +} + +export function insights(): Insight[] { + return ( + base().prepare(`SELECT * FROM insights ORDER BY numero`).all() as Record< + string, + unknown + >[] + ).map((ligne) => ({ + numero: ligne.numero as number, + titre: ligne.titre as string, + corps: ligne.corps as string, + implications: ligne.implications as string | null, + confiance: ligne.confiance as string, + derive_de: JSON.parse((ligne.derive_de as string) || '[]') + })); +} + +export interface Changement { + horodatage: string; + mode: string; + texte_id: string | null; + titre: string | null; + nature: string; + champ: string | null; + ancienne_valeur: string | null; + nouvelle_valeur: string | null; + description: string | null; +} + +/** Derniers changements détectés par le pipeline, pour l'accueil. */ +export function derniersChangements(limite = 12): Changement[] { + return base() + .prepare( + `SELECT v.horodatage, v.mode, c.texte_id, t.titre_court AS titre, + c.nature, c.champ, c.ancienne_valeur, c.nouvelle_valeur, c.description + FROM veille_changements c + JOIN veille_log v ON v.id = c.run_id + LEFT JOIN textes t ON t.id = c.texte_id + ORDER BY v.horodatage DESC, c.id DESC + LIMIT ?` + ) + .all(limite) as Changement[]; +} + +export interface Run { + horodatage: string; + mode: string; + duree_s: number | null; + ajouts: number; + modifications: number; + echecs: number; + rapport: string | null; +} + +export function dernierRun(): Run | null { + return ( + (base() + .prepare( + `SELECT horodatage, mode, duree_s, ajouts, modifications, echecs, rapport + FROM veille_log ORDER BY horodatage DESC, id DESC LIMIT 1` + ) + .get() as Run | undefined) ?? null + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Hydratation +// ───────────────────────────────────────────────────────────────────────────── +function hydrater(ligne: Record): Texte { + return { + ...(ligne as unknown as Texte), + themes: JSON.parse((ligne.themes as string) || '[]'), + impacts: JSON.parse((ligne.impacts as string) || '{}'), + points_cles: JSON.parse((ligne.points_cles as string) || '[]') + }; +} diff --git a/web/src/lib/styles/base.css b/web/src/lib/styles/base.css new file mode 100644 index 0000000..358bc5c --- /dev/null +++ b/web/src/lib/styles/base.css @@ -0,0 +1,342 @@ +/** + * Socle : polices auto-hébergées, réinitialisation, éléments communs. + * + * Aucune requête tierce. Les `@font-face` pointent vers `static/polices/`, + * copiées depuis npm par `npm run polices`. + */ + +/* ── Polices ─────────────────────────────────────────────────────────────── */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/polices/archivo-latin-400-normal.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F, U+20AC; +} +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/polices/archivo-latin-600-normal.woff2') format('woff2'); +} +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/polices/archivo-latin-700-normal.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/polices/inter-latin-400-normal.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/polices/inter-latin-500-normal.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/polices/inter-latin-600-normal.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/polices/inter-latin-700-normal.woff2') format('woff2'); +} + +/* ── Réinitialisation ────────────────────────────────────────────────────── */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + -webkit-text-size-adjust: 100%; + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--noir-oki); + color: var(--blanc-creme); + font-family: var(--font-body); + font-size: 1rem; + line-height: 1.55; + font-weight: 400; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +h1, +h2, +h3, +h4 { + font-family: var(--font-display); + font-weight: 700; + letter-spacing: -0.01em; + line-height: 1.15; + margin: 0 0 var(--space-2); + text-wrap: balance; +} + +h1 { + font-size: clamp(1.55rem, 3.4vw, 2.3rem); + text-transform: uppercase; +} +h2 { + font-size: clamp(1.2rem, 2.4vw, 1.55rem); + text-transform: uppercase; +} +h3 { + font-size: 1.05rem; +} +h4 { + font-size: 0.95rem; +} + +p { + margin: 0 0 var(--space-2); + text-wrap: pretty; +} + +a { + color: var(--or-oki); + text-decoration-thickness: 1px; + text-underline-offset: 0.18em; +} +a:hover { + color: var(--or-clair); +} + +:focus-visible { + outline: 3px solid var(--or-oki); + outline-offset: 2px; + border-radius: var(--radius-sm); +} + +img, +svg { + max-width: 100%; + height: auto; +} + +mark { + background: color-mix(in srgb, var(--or-oki) 26%, transparent); + color: inherit; + padding: 0 0.15em; + border-radius: 2px; + font-weight: 600; +} + +hr { + border: 0; + border-top: 1px solid var(--line); + margin: var(--space-4) 0; +} + +/* ── Mise en page ────────────────────────────────────────────────────────── */ +.contenant { + width: min(100% - 2rem, var(--container)); + margin-inline: auto; +} + +.section { + padding-block: var(--space-4); +} + +.pile { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.ligne { + display: flex; + flex-wrap: wrap; + gap: var(--space-1); + align-items: center; +} + +.grille { + display: grid; + gap: var(--space-2); + grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); +} + +/* ── Signature de marque ─────────────────────────────────────────────────── */ +.flag-bar { + height: 6px; + background: linear-gradient( + to right, + #0d0d0d 0 25%, + var(--or-oki) 25% 50%, + var(--vert-oki) 50% 75%, + var(--rouge-oki) 75% 100% + ); +} +:root.light-theme .flag-bar { + background: linear-gradient( + to right, + #000 0 25%, + var(--or-oki) 25% 50%, + var(--vert-oki) 50% 75%, + var(--rouge-oki) 75% 100% + ); +} + +/* ── Carte canonique ─────────────────────────────────────────────────────── */ +.carte { + background: var(--card-bg); + border: var(--border-card); + border-left: 4px solid var(--or-oki); + border-radius: var(--radius-md); + padding: var(--space-2); + transition: + transform var(--dur-tanbou) var(--ease-ka), + border-color var(--dur-tanbou) var(--ease-ka), + background var(--dur-tanbou) var(--ease-ka); +} +.carte:hover { + background: var(--card-bg-survol); + transform: translateY(-2px); +} + +/* ── Tag canonique ───────────────────────────────────────────────────────── */ +.tag { + display: inline-flex; + align-items: center; + gap: 0.35em; + background: color-mix(in srgb, var(--or-oki) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--or-oki) 55%, transparent); + border-radius: var(--radius-sm); + color: var(--blanc-creme); + font-size: 0.78rem; + padding: 0.15em 0.55em; + text-decoration: none; + white-space: nowrap; +} + +/* ── Bouton canonique ────────────────────────────────────────────────────── */ +.bouton { + position: relative; + display: inline-flex; + align-items: center; + gap: 0.5em; + font-family: var(--font-display); + font-weight: 700; + font-size: 0.82rem; + text-transform: uppercase; + letter-spacing: 0.03em; + padding: 0.55em 1.1em; + border: var(--border-btn); + border-radius: var(--radius-sm); + background: transparent; + color: var(--or-oki); + cursor: pointer; + text-decoration: none; + overflow: hidden; + transition: color var(--dur-tanbou) var(--ease-syncope); +} +.bouton::after { + content: ''; + position: absolute; + inset: 0; + background: var(--or-oki); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--dur-tanbou) var(--ease-syncope); + z-index: -1; +} +.bouton:hover { + color: var(--noir-oki); +} +.bouton:hover::after { + transform: scaleX(1); +} +.bouton > * { + position: relative; + z-index: 1; +} + +.bouton-discret { + border-color: var(--line); + color: var(--blanc-creme); +} + +/* ── Accessibilité ───────────────────────────────────────────────────────── */ +.lien-evitement { + position: absolute; + left: -9999px; + top: 0; + background: var(--or-oki); + color: var(--noir-oki); + padding: var(--space-1) var(--space-2); + font-weight: 700; + z-index: 100; +} +.lien-evitement:focus { + left: 0; +} + +.visuellement-cache { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + +/* Une seule garde, au niveau global : sans mouvement, tout reste lisible. */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + .carte:hover { + transform: none; + } +} + +/* ── Impression : la veille se transmet aussi sur papier ─────────────────── */ +@media print { + :root { + --noir-oki: #fff; + --blanc-creme: #000; + --card-bg: transparent; + --line: #999; + --muted: #444; + } + .entete-site, + .pied-site, + .panneau-facettes, + .bouton { + display: none !important; + } + a[href^='http']::after { + content: ' (' attr(href) ')'; + font-size: 0.75em; + word-break: break-all; + } +} diff --git a/web/src/lib/styles/oki-tokens.css b/web/src/lib/styles/oki-tokens.css new file mode 100644 index 0000000..8963b4c --- /dev/null +++ b/web/src/lib/styles/oki-tokens.css @@ -0,0 +1,104 @@ +/** + * Tokens OKI — Organisation KA Internationale. + * + * Valeurs mesurées sur o-k-i.net, reprises telles quelles de la charte. Le + * thème sombre est l'identité par défaut ; le clair est une variante opt-in qui + * assombrit les accents pour rester au contraste AA. + * + * Trois règles ne se négocient pas : + * · l'or porte toute l'interaction — c'est la seule couleur d'action ; + * · rouge = signaler, vert = valider, or = agir : jamais permuter ; + * · angles nets partout, 3 px sur les petits éléments, 6 px sur les cartes. + * + * Usage propre à cette application : c'est un outil de veille, pas une vitrine. + * La densité informationnelle prime, le motion est réduit au strict minimum, et + * la flag-bar n'apparaît qu'une fois par écran. + */ + +:root { + /* ── Noyau ── */ + --noir-oki: #0d0d0d; + --noir-profond: #1a0f1a; + --blanc-creme: #fff8e7; + --line: rgba(255, 255, 255, 0.1); + --or-oki: #fdb813; + --rouge-oki: #ff1654; + + /* ── Étendue, avec parcimonie ── */ + --vert-oki: #00d66c; + --turquoise-caraibes: #00ced1; + --jaune-soleil: #ffd700; + --orange-flamme: #ff6b35; + --violet-nuit: #6b2d5c; + --bleu-ocean: #0077be; + --or-clair: #ffe066; + --gris-sombre: #2d1b2e; + + /* ── Sémantique dérivée ── */ + --muted: color-mix(in srgb, var(--blanc-creme) 70%, transparent); + --muted-fort: color-mix(in srgb, var(--blanc-creme) 55%, transparent); + --card-bg: rgba(255, 255, 255, 0.03); + --card-bg-survol: rgba(255, 255, 255, 0.06); + + /* ── Typographie ── */ + --font-display: 'Archivo', 'Arial Black', sans-serif; + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-mono: ui-monospace, 'SFMono-Regular', 'Menlo', monospace; + + /* ── Formes et espacements ── */ + --radius-sm: 3px; + --radius-md: 6px; + --border-card: 1px solid var(--line); + --border-btn: 2px solid var(--or-oki); + --space-1: 0.75rem; + --space-2: 1rem; + --space-3: 1.5rem; + --space-4: 2rem; + --space-5: 4rem; + --container: 1200px; + + /* ── Motion : les cadences gwoka ── */ + --ease-ka: cubic-bezier(0.22, 1, 0.36, 1); + --ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); + --dur-tanbou: 120ms; + --dur-mesure: 480ms; + --dur-phrase: 960ms; + + /* ── Sémantique métier ── + * Les statuts sont d'abord distingués par leur libellé et leur forme, la + * couleur ne fait que renforcer : un daltonien doit pouvoir lire la page. + */ + --statut-promulguee: var(--vert-oki); + --statut-attente: var(--or-oki); + --statut-cc: var(--orange-flamme); + --statut-navette: var(--turquoise-caraibes); + --statut-projet: var(--muted-fort); + --statut-rejetee: var(--rouge-oki); + + --impact-positif: var(--vert-oki); + --impact-negatif: var(--rouge-oki); + --impact-mixte: var(--jaune-soleil); + --impact-neutre: var(--muted-fort); +} + +/* Thème clair — opt-in, accents assombris pour le contraste AA. */ +:root.light-theme { + --noir-oki: #fff8e7; + --blanc-creme: #0d0d0d; + --noir-profond: #f5f0e8; + --gris-sombre: #e8ddd0; + --or-oki: #8f5c00; + --vert-oki: #006b3d; + --rouge-oki: #a01030; + --turquoise-caraibes: #006b75; + --bleu-ocean: #004b7f; + --or-clair: #a86e00; + --jaune-soleil: #7a5300; + --orange-flamme: #b34700; + --violet-nuit: #4a1f40; + --line: rgba(0, 0, 0, 0.14); + --card-bg: rgba(0, 0, 0, 0.025); + --card-bg-survol: rgba(0, 0, 0, 0.05); + --muted: color-mix(in srgb, var(--blanc-creme) 72%, transparent); + --muted-fort: color-mix(in srgb, var(--blanc-creme) 58%, transparent); +} diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts new file mode 100644 index 0000000..d9e3b25 --- /dev/null +++ b/web/src/lib/types.ts @@ -0,0 +1,180 @@ +/** Vocabulaire du domaine, aligné sur le schéma SQLite et les modèles pydantic. */ + +export type Statut = + | 'promulguee' + | 'adoptee_non_promulguee' + | 'saisie_cc' + | 'validee_cc' + | 'censuree_partiellement' + | 'navette' + | 'deposee_non_examinee' + | 'annoncee' + | 'rejetee'; + +export type TypeTexte = + | 'loi' + | 'loi_organique' + | 'pjl' + | 'ppl' + | 'ordonnance' + | 'decision_cc' + | 'decret' + | 'accord_international'; + +export type Theme = + | 'justice' + | 'securite' + | 'numerique' + | 'social' + | 'fiscal' + | 'environnement' + | 'agriculture' + | 'sante' + | 'memoire_patrimoine' + | 'outre_mer' + | 'economie' + | 'migration' + | 'institutions'; + +export type Public = 'individus' | 'associations' | 'entreprises'; +export type SensImpact = 'positif' | 'negatif' | 'mixte' | 'neutre'; +export type Pertinence = 'forte' | 'moyenne' | 'faible'; +export type Confiance = 'high' | 'medium' | 'low'; +export type RegimeLibertes = 'droits_plus' | 'controle_plus' | 'mixte' | 'neutre'; + +export interface Impact { + sens: SensImpact; + note: string; +} + +export interface Source { + url: string; + titre: string | null; + editeur: string | null; + date_publication: string | null; + tier: 'T1' | 'T2'; + extrait_verbatim: string | null; + contexte: string | null; + confiance: Confiance | null; + marqueur: string | null; + fichier_origine: string | null; +} + +export interface Evenement { + date_evenement: string; + type_etape: string; + description: string; + source_url: string | null; + previsionnel: 0 | 1; +} + +export interface DecisionCC { + numero_affaire: string; + date_saisine: string | null; + date_decision: string | null; + resultat: string | null; + saisissants: string | null; + resume: string | null; + url: string | null; + date_decision_attendue: string | null; +} + +export interface Texte { + id: string; + numero_officiel: string | null; + type: TypeTexte; + titre_court: string; + titre_officiel: string | null; + statut: Statut; + statut_date: string | null; + date_depot: string | null; + date_adoption: string | null; + date_promulgation: string | null; + date_entree_vigueur: string | null; + prochaine_echeance: string | null; + prochaine_echeance_label: string | null; + themes: Theme[]; + impacts: Partial>; + regime_libertes: RegimeLibertes | null; + regime_libertes_note: string | null; + guadeloupe_pertinence: Pertinence | null; + guadeloupe_note: string | null; + resume: string | null; + points_cles: string[]; + confiance: Confiance; + source_seed: string | null; + a_verifier: 0 | 1; + motif_verification: string | null; + derniere_verif: string | null; + nb_sources: number; + nb_evenements: number; + devant_cc: 0 | 1; + /** Extrait surligné, présent uniquement sur les résultats de recherche. */ + extrait?: string | null; +} + +export interface TexteDetaille extends Texte { + sources: Source[]; + evenements: Evenement[]; + decisions_cc: DecisionCC[]; +} + +export interface Echeance { + id: number; + code: string; + date_echeance: string; + libelle: string; + description: string | null; + portee: 'nationale' | 'texte'; + texte_id: string | null; + concerne_guadeloupe: 0 | 1 | null; + note_guadeloupe: string | null; + previsionnel: 0 | 1; + source_url: string | null; + source_extrait: string | null; +} + +export interface Insight { + numero: number; + titre: string; + corps: string; + implications: string | null; + confiance: string; + derive_de: string[]; +} + +export interface Facette { + valeur: string; + libelle: string; + nombre: number; + actif: boolean; +} + +export interface Filtres { + q: string; + statut: string[]; + theme: string[]; + type: string[]; + impact: string[]; + guadeloupe: string[]; + confiance: string[]; + from: string | null; + to: string | null; + tri: 'date' | 'pertinence'; + page: number; +} + +export interface Resultats { + textes: Texte[]; + total: number; + page: number; + pages: number; + facettes: { + statut: Facette[]; + theme: Facette[]; + type: Facette[]; + guadeloupe: Facette[]; + impact: Facette[]; + confiance: Facette[]; + }; +} diff --git a/web/src/routes/+error.svelte b/web/src/routes/+error.svelte new file mode 100644 index 0000000..ca9cd77 --- /dev/null +++ b/web/src/routes/+error.svelte @@ -0,0 +1,57 @@ + + + + {page.status} · Veille législative Gwadloup + + + +
+

{page.status}

+ {#if page.status === 404} +

Pa ni ayen la

+

+ Cette page n'existe pas — ou n'existe plus. Le texte que vous cherchez a peut-être un + autre identifiant. +

+ {:else} +

Sa pa maché

+

{page.error?.message ?? 'Une erreur est survenue.'}

+ {/if} + + +
+ + diff --git a/web/src/routes/+layout.server.ts b/web/src/routes/+layout.server.ts new file mode 100644 index 0000000..fd4fe67 --- /dev/null +++ b/web/src/routes/+layout.server.ts @@ -0,0 +1,9 @@ +/** + * Le rendu serveur est le défaut sur toute l'application. + * + * On ne pose JAMAIS `ssr = false` ici : le mode SPA est explicitement écarté + * par l'architecture de rendu (§3bis), pour le référencement comme pour la + * lisibilité sans JavaScript. Le prérendu est activé page par page, uniquement + * sur celles dont le contenu est figé (`/a-propos`, `/methode`). + */ +export const prerender = false; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte new file mode 100644 index 0000000..e859bd0 --- /dev/null +++ b/web/src/routes/+layout.svelte @@ -0,0 +1,220 @@ + + +Aller au contenu + +
+
+ + + + Veille législative + Gwadloup · libertés, associations, entreprises + + + + + + +
+ +
+
+ +
+ {@render children()} +
+ +
+
+

+ Aucune donnée législative n'est inventée. Chaque fait porte sa source, sa date et son + niveau de confiance. Ce qui n'est pas établi est marqué « à vérifier ». +

+

+ + Un service libre opéré par + ORGANISATION KA INTERNATIONALE + · o-k-i.net +

+
+
+ + diff --git a/web/src/routes/+page.server.ts b/web/src/routes/+page.server.ts new file mode 100644 index 0000000..99c97d3 --- /dev/null +++ b/web/src/routes/+page.server.ts @@ -0,0 +1,26 @@ +import { + compteurs, + derniersChangements, + dernierRun, + decisionsAttendues, + insights, + prochainesEcheances +} from '$serveur/requetes'; +import { DATE_ARRETE_CORPUS } from '$serveur/db'; +import type { PageServerLoad } from './$types'; + +/** + * Tableau de bord — rendu serveur, sans prérendu. + * + * Les compteurs et les derniers changements doivent refléter le dernier + * passage du pipeline sans qu'on ait à reconstruire le site. + */ +export const load: PageServerLoad = async () => ({ + compteurs: compteurs(), + echeances: prochainesEcheances(45), + decisionsAttendues: decisionsAttendues(), + changements: derniersChangements(8), + run: dernierRun(), + insights: insights(), + dateArrete: DATE_ARRETE_CORPUS +}); diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte new file mode 100644 index 0000000..4a2c0bf --- /dev/null +++ b/web/src/routes/+page.svelte @@ -0,0 +1,362 @@ + + + + Veille législative Gwadloup — tableau de bord + + + +
+

Ki lwa ka chanjé lavi an nou ?

+

+ {data.compteurs.total} textes suivis, {data.compteurs.sources} sources vérifiables. + Données arrêtées au {dateLongue(data.dateArrete)}. +

+ + +
+ +
+

Où en sont les textes

+
+ {#each data.compteurs.parStatut as ligne (ligne.valeur)} + + {ligne.nombre} + {ligne.libelle} + + {/each} +
+ + +
+ +
+
+

Prochaines échéances

+ {#if data.echeances.length} +
    + {#each data.echeances as echeance (echeance.code)} + {@const jours = joursRestants(echeance.date_echeance, data.dateArrete)} +
  1. +
    + + {#if jours >= 0}dans {jours} j{/if} +
    +

    + {#if echeance.texte_id} + {echeance.libelle} + {:else} + {echeance.libelle} + {/if} +

    + {#if echeance.description}

    {echeance.description}

    {/if} + {#if echeance.concerne_guadeloupe === 0} +

    Ne concerne pas la Guadeloupe.

    + {/if} + {#if echeance.note_guadeloupe} +

    {echeance.note_guadeloupe}

    + {/if} +
  2. + {/each} +
+

Voir tout le calendrier →

+ {:else} +

Aucune échéance dans les 45 prochains jours.

+ {/if} +
+ +
+
+

Point de veille

+ {#if analyse} +
+

{analyse.titre}

+

{analyse.corps.slice(0, 460)}{analyse.corps.length > 460 ? '…' : ''}

+ {#if analyse.implications} +

Implications — {analyse.implications}

+ {/if} + +
+ {/if} +
+ +
+

Décisions attendues

+
    + {#each data.decisionsAttendues as decision (decision.numero_affaire)} +
  • + {decision.numero_affaire} + {#if decision.texte_id} + {decision.libelle} + {:else} + {decision.libelle} + {/if} + {#if decision.date_echeance} + → {dateLongue(decision.date_echeance)} + {/if} +
  • + {/each} +
+
+ +
+

Derniers changements détectés

+ {#if data.changements.length} +
    + {#each data.changements as changement, i (i)} +
  • + {#if changement.texte_id} + {changement.titre ?? changement.texte_id} + {:else} + {changement.description} + {/if} + {#if changement.champ} + + {changement.champ} : {changement.ancienne_valeur ?? '—'} → + {changement.nouvelle_valeur} + + {/if} +
  • + {/each} +
+ {:else} +

+ Aucun changement enregistré. Le pipeline n'a pas encore tourné en mode écriture — + lancer make update. +

+ {/if} + {#if data.run} +

+ Dernier passage : {data.run.horodatage} ({data.run.mode}), + {data.run.ajouts} ajout(s), {data.run.modifications} modification(s). +

+ {/if} +
+
+
+ + diff --git a/web/src/routes/a-propos/+page.svelte b/web/src/routes/a-propos/+page.svelte new file mode 100644 index 0000000..1b739f3 --- /dev/null +++ b/web/src/routes/a-propos/+page.svelte @@ -0,0 +1,121 @@ + + À propos · Veille législative Gwadloup + + + +

À propos

+ +
+

+ Cette plateforme suit la production législative française et ce qu'elle change aux libertés + des individus, des associations et des entreprises — avec un zoom Guadeloupe et outre-mer. +

+ +

Pourquoi

+

+ Les textes qui restreignent ou étendent des libertés se lisent rarement dans un seul journal. + Ils arrivent par morceaux : un article dans une loi de simplification, un cavalier censuré + dans une loi de finances, un décret d'application six mois plus tard. Suivre cela demande du + temps que peu d'associations et de petites structures ont. +

+

+ Cette veille rassemble ce travail au même endroit, avec les sources en clair, et signale + explicitement ce qui n'est pas établi. +

+ +

Ce que la plateforme ne fait pas

+
    +
  • Elle ne donne pas de conseil juridique. Un texte cité ici ne remplace pas un avocat.
  • +
  • + Elle n'invente rien. Quand une information manque, elle manque : la fiche le dit, plutôt + que de combler le vide. +
  • +
  • + Elle ne suit pas votre navigation. Aucun traceur, aucun cookie de mesure, aucune requête + vers un service tiers — polices comprises. +
  • +
+ +

Technique

+

+ Tout est libre et auto-hébergeable sur un serveur modeste : un pipeline Python qui lit les + sources officielles, une base SQLite unique, une application SvelteKit rendue côté serveur. + La recherche plein texte est assurée par SQLite lui-même. Aucun service externe n'est + nécessaire au fonctionnement. +

+

+ Le code est sous licence AGPL. La sauvegarde consiste à copier un fichier. +

+ +

Reprendre les données

+

+ Une interface de programmation en lecture est ouverte, sans clé ni inscription : +

+
    +
  • GET /api/textes — liste filtrable
  • +
  • GET /api/textes/[identifiant] — fiche complète avec sources
  • +
  • GET /api/echeances — calendrier
  • +
+

+ Si vous réutilisez ces données, citez la source primaire plutôt que cette plateforme : elle + n'est qu'un intermédiaire. +

+ +

Contact

+

+ Une erreur, un texte manquant, une source cassée ? Signalez-le — une veille se corrige à + plusieurs. cyber.mawonaj@gmail.com +

+ +

+ Lire la méthode en détail +

+
+ + diff --git a/web/src/routes/a-propos/+page.ts b/web/src/routes/a-propos/+page.ts new file mode 100644 index 0000000..7feb7d7 --- /dev/null +++ b/web/src/routes/a-propos/+page.ts @@ -0,0 +1,5 @@ +/** + * Page figée : aucun contenu dynamique, donc exclue du manifeste du rendu + * serveur et servie en statique (§3bis de l'architecture de rendu). + */ +export const prerender = true; diff --git a/web/src/routes/api/echeances/+server.ts b/web/src/routes/api/echeances/+server.ts new file mode 100644 index 0000000..f2c1f6c --- /dev/null +++ b/web/src/routes/api/echeances/+server.ts @@ -0,0 +1,26 @@ +import { json } from '@sveltejs/kit'; +import { calendrier, decisionsAttendues, prochainesEcheances } from '$serveur/requetes'; +import type { RequestHandler } from './$types'; + +/** + * `GET /api/echeances` — calendrier. + * + * `?jours=N` restreint aux N prochains jours ; sans paramètre, tout le + * calendrier est rendu, décisions attendues comprises. + */ +export const GET: RequestHandler = async ({ url }) => { + const jours = Number.parseInt(url.searchParams.get('jours') ?? '', 10); + + return json( + { + echeances: Number.isFinite(jours) && jours > 0 ? prochainesEcheances(jours) : calendrier(), + decisions_attendues: decisionsAttendues() + }, + { + headers: { + 'cache-control': 'public, max-age=300', + 'access-control-allow-origin': '*' + } + } + ); +}; diff --git a/web/src/routes/api/textes/+server.ts b/web/src/routes/api/textes/+server.ts new file mode 100644 index 0000000..7ab4b4f --- /dev/null +++ b/web/src/routes/api/textes/+server.ts @@ -0,0 +1,36 @@ +import { json } from '@sveltejs/kit'; +import { lireFiltres, rechercher } from '$serveur/requetes'; +import type { RequestHandler } from './$types'; + +/** + * `GET /api/textes` — liste filtrable. + * + * Mêmes paramètres que la page de recherche : `q`, `statut`, `theme`, `type`, + * `impact`, `guadeloupe`, `confiance`, `from`, `to`, `tri`, `page`. Les + * paramètres multivalués acceptent la répétition (`?theme=sante&theme=social`) + * comme la virgule (`?theme=sante,social`). + * + * Ouverte sans clé : ces données sont publiques et proviennent de sources + * publiques. Le cache est court — le pipeline peut écrire à tout moment. + */ +export const GET: RequestHandler = async ({ url }) => { + const filtres = lireFiltres(url.searchParams); + const resultats = rechercher(filtres); + + return json( + { + filtres, + total: resultats.total, + page: resultats.page, + pages: resultats.pages, + textes: resultats.textes, + facettes: resultats.facettes + }, + { + headers: { + 'cache-control': 'public, max-age=300', + 'access-control-allow-origin': '*' + } + } + ); +}; diff --git a/web/src/routes/api/textes/[slug]/+server.ts b/web/src/routes/api/textes/[slug]/+server.ts new file mode 100644 index 0000000..afe381e --- /dev/null +++ b/web/src/routes/api/textes/[slug]/+server.ts @@ -0,0 +1,16 @@ +import { error, json } from '@sveltejs/kit'; +import { texte } from '$serveur/requetes'; +import type { RequestHandler } from './$types'; + +/** `GET /api/textes/[slug]` — fiche complète, sources et timeline comprises. */ +export const GET: RequestHandler = async ({ params }) => { + const fiche = texte(params.slug); + if (!fiche) error(404, { message: `Texte inconnu : ${params.slug}` }); + + return json(fiche, { + headers: { + 'cache-control': 'public, max-age=300', + 'access-control-allow-origin': '*' + } + }); +}; diff --git a/web/src/routes/calendrier/+page.server.ts b/web/src/routes/calendrier/+page.server.ts new file mode 100644 index 0000000..d967613 --- /dev/null +++ b/web/src/routes/calendrier/+page.server.ts @@ -0,0 +1,22 @@ +import { calendrier, decisionsAttendues } from '$serveur/requetes'; +import { DATE_ARRETE_CORPUS } from '$serveur/db'; +import type { PageServerLoad } from './$types'; + +/** Calendrier — rendu serveur : les échéances vivent avec la base. */ +export const load: PageServerLoad = async () => { + const echeances = calendrier(); + + // Regroupement par mois, dans l'ordre chronologique. + const parMois = new Map(); + for (const echeance of echeances) { + const cle = echeance.date_echeance.slice(0, 7); + if (!parMois.has(cle)) parMois.set(cle, []); + parMois.get(cle)!.push(echeance); + } + + return { + mois: [...parMois.entries()].map(([cle, liste]) => ({ cle, echeances: liste })), + decisions: decisionsAttendues(), + dateArrete: DATE_ARRETE_CORPUS + }; +}; diff --git a/web/src/routes/calendrier/+page.svelte b/web/src/routes/calendrier/+page.svelte new file mode 100644 index 0000000..c1b2ccc --- /dev/null +++ b/web/src/routes/calendrier/+page.svelte @@ -0,0 +1,209 @@ + + + + Calendrier des échéances · Veille législative Gwadloup + + + +

Calendrier

+

+ Échéances de la fenêtre de veille. Les dates postérieures au + {dateLongue(data.dateArrete)} sont prévisionnelles : elles indiquent ce qui est + attendu, jamais ce qui est acquis. +

+ +{#if data.decisions.length} +
+

Décisions attendues du Conseil constitutionnel

+
    + {#each data.decisions as decision (decision.numero_affaire)} +
  • + {decision.numero_affaire} +
    + {#if decision.texte_id} + {decision.libelle} + {:else} + {decision.libelle} + {/if} + {#if decision.date_echeance} +

    Attendue vers le {dateLongue(decision.date_echeance)}

    + {/if} +
    +
  • + {/each} +
+
+{/if} + +{#each data.mois as bloc (bloc.cle)} +
+

{libelleMois(bloc.cle + '-01')}

+
    + {#each bloc.echeances as echeance (echeance.code)} + {@const jours = joursRestants(echeance.date_echeance, data.dateArrete)} +
  1. +
    + + {#if echeance.previsionnel === 1} + prévisionnel + {/if} +
    +
    +

    + {#if echeance.texte_id} + {echeance.libelle} + {:else} + {echeance.libelle} + {/if} +

    + {#if echeance.description}

    {echeance.description}

    {/if} + {#if echeance.concerne_guadeloupe === 0} +

    Ne concerne pas la Guadeloupe

    + {:else if echeance.concerne_guadeloupe === 1} +

    Concerne la Guadeloupe

    + {/if} + {#if echeance.note_guadeloupe}

    {echeance.note_guadeloupe}

    {/if} + {#if echeance.source_extrait} +
    {echeance.source_extrait}
    + {/if} + {#if echeance.source_url} + + Source + + {/if} +
    +
  2. + {/each} +
+
+{/each} + + diff --git a/web/src/routes/methode/+page.svelte b/web/src/routes/methode/+page.svelte new file mode 100644 index 0000000..56a7c32 --- /dev/null +++ b/web/src/routes/methode/+page.svelte @@ -0,0 +1,206 @@ + + Méthode · Veille législative Gwadloup + + + +

Méthode

+ +
+

+ Une veille législative ne vaut que par ce qu'elle refuse d'affirmer. Cette page dit d'où + viennent les données, comment elles sont qualifiées, et où sont leurs limites. +

+ +

La règle d'or : le statut prime sur l'intitulé

+

+ Un texte « voté » n'est pas un texte applicable. Entre le vote et l'entrée en vigueur, il + peut être censuré, amputé, ou attendre des décrets pendant des mois. La confusion entre ces + états est la première source d'erreur dans le débat public — et elle change tout pour qui + veut savoir ce qui s'applique aujourd'hui. +

+ +
+
Promulguée
+
+ Le texte porte un numéro de loi et a été publié au Journal officiel. Il produit ses + effets, sous réserve de ses propres dates d'entrée en vigueur différées. +
+ +
Adoptée, non promulguée
+
+ Le Parlement a voté définitivement, mais le texte n'est pas signé. Il n'a pas de numéro. + Il ne s'applique pas. Une saisine du Conseil constitutionnel suspend la promulgation + pendant un mois. +
+ +
Devant le Conseil constitutionnel
+
+ Une affaire est enregistrée et la décision n'est pas rendue. Le texte peut en sortir + intact, amputé, ou annulé. Tant que la décision n'est pas rendue, son contenu applicable + est inconnu. +
+ +
Validée, en attente de promulgation
+
+ Le Conseil a statué favorablement ; il ne manque que la signature. La promulgation est + très probable mais pas encore intervenue. +
+ +
En navette
+
+ Le texte fait l'aller-retour entre l'Assemblée et le Sénat. Son contenu bouge encore. + Rien de ce qu'il contient n'est acquis. +
+ +
Déposée, non examinée · Annoncée
+
+ Le texte existe sur le papier — ou seulement dans une déclaration. Il documente une + intention politique, pas le droit positif. +
+
+ +

D'où viennent les données

+

+ Le socle est un corpus de recherche arrêté au 25 juillet 2026, couvrant la + production législative du 1er mai au 30 septembre 2026 : un rapport consolidé de + onze chapitres, douze rapports de dimension thématique, une bibliographie de 481 références, + et un fichier d'arbitrage des désaccords tranchés sur sources primaires. +

+

+ Chaque fait importé conserve sa citation d'origine — adresse, date, extrait verbatim — et son + niveau de confiance. Les marqueurs de citation du rapport sont résolus vers leur source : + aucune affirmation n'entre en base sans que l'on puisse remonter à ce qui l'atteste. +

+ +

Comment la base se met à jour

+

+ Un pipeline interroge régulièrement les sources officielles : Légifrance par son interface de + programmation publique, la liste chronologique des lois promulguées du Sénat, et le registre + des affaires du Conseil constitutionnel. Chaque source est interrogée séparément ; si l'une + est indisponible, les autres continuent. +

+

+ Un texte collecté est rapproché de la base par son numéro officiel, à défaut par similarité + de titre. Entre deux seuils, le rapprochement est signalé mais pas appliqué : + mieux vaut une vérification humaine qu'une fusion erronée. En cas de contradiction entre + sources, la source primaire l'emporte — Journal officiel et Légifrance devant les assemblées, + les assemblées devant la presse. +

+ +

Ce qui est déduit, et signalé comme tel

+

+ Certaines informations ne figurent nulle part explicitement et sont déduites. Elles portent + alors la mention « à vérifier » et un motif lisible sur la fiche : +

+
    +
  • + Pertinence Guadeloupe des textes non promulgués. Le rapport ne cote que + les lois promulguées ; pour les autres, la cotation vient d'une détection de vocabulaire. +
  • +
  • + Sens des impacts par public. Le rapport consacre un chapitre aux + individus, un aux associations, un aux entreprises : le placement d'un texte dans l'un de + ces chapitres est un choix éditorial explicite, et sert de base à la ventilation. Le sens + — favorable, défavorable, mixte — vient de la cotation manuelle quand elle existe, du + vocabulaire sinon. +
  • +
  • + Thèmes. Déduits par mots-clés sur l'intitulé et le résumé. +
  • +
  • + Textes ajoutés par le pipeline. Ils entrent avec une confiance basse et + le drapeau de revue : la machine propose, un humain valide. +
  • +
+ +

Les échéances sont des prévisions

+

+ Toute date postérieure au 25 juillet 2026 est prévisionnelle, et affichée comme telle. Une + décision « attendue vers le 24 août » peut tomber avant, après, ou être reportée. Les + libellés du corpus — « fin août », « automne 2026 » — sont conservés tels quels ; la date + associée ne sert qu'au tri et au filtrage. +

+ +

Limites connues

+
    +
  • + Le corpus s'arrête au 25 juillet 2026. Tout ce qui suit dépend des passages du pipeline. +
  • +
  • + Certaines références du corpus n'ont pas d'adresse consultable — surtout dans la + dimension consacrée aux associations. Elles sont conservées comme références + bibliographiques, sans lien. +
  • +
  • + Le contenu du titre « adaptation outre-mer » de la loi « Riposte » n'était pas public à + la date d'arrêté. Les effets ultramarins de ce texte sont donc incomplets. +
  • +
  • + La veille suit les lois, pas les décrets d'application. Or c'est souvent au niveau + réglementaire que se jouent les effets concrets — les décrets chlordécone en sont + l'exemple. +
  • +
+ +

Corriger une erreur

+

+ Si une fiche est fausse, elle doit être corrigée, pas défendue. + Signalez-la en indiquant l'identifiant du texte + et la source qui vous donne raison. +

+
+ + diff --git a/web/src/routes/methode/+page.ts b/web/src/routes/methode/+page.ts new file mode 100644 index 0000000..72c0015 --- /dev/null +++ b/web/src/routes/methode/+page.ts @@ -0,0 +1,2 @@ +/** Page figée, prérendue au build — voir a-propos/+page.ts. */ +export const prerender = true; diff --git a/web/src/routes/recherche/+page.server.ts b/web/src/routes/recherche/+page.server.ts new file mode 100644 index 0000000..0ad6b96 --- /dev/null +++ b/web/src/routes/recherche/+page.server.ts @@ -0,0 +1,34 @@ +import { basculerFacette, lireFiltres, rechercher } from '$serveur/requetes'; +import type { PageServerLoad } from './$types'; + +/** + * Recherche à facettes — rendu serveur, jamais prérendue. + * + * Les facettes vivent dans `url.searchParams` : une URL de recherche est + * partageable et se recharge à l'identique. Le prérendu interdit d'y accéder, + * c'est pourquoi cette route reste en rendu serveur (§3bis). + */ +export const load: PageServerLoad = async ({ url }) => { + const filtres = lireFiltres(url.searchParams); + const resultats = rechercher(filtres); + + return { + filtres, + resultats, + // Les liens de facette sont calculés côté serveur : la page fonctionne + // entièrement sans JavaScript. + liens: Object.fromEntries( + (['statut', 'theme', 'type', 'impact', 'guadeloupe', 'confiance'] as const).map( + (champ) => [ + champ, + Object.fromEntries( + resultats.facettes[champ === 'guadeloupe' ? 'guadeloupe' : champ].map((f) => [ + f.valeur, + basculerFacette(filtres, champ, f.valeur) + ]) + ) + ] + ) + ) + }; +}; diff --git a/web/src/routes/recherche/+page.svelte b/web/src/routes/recherche/+page.svelte new file mode 100644 index 0000000..7eddf19 --- /dev/null +++ b/web/src/routes/recherche/+page.svelte @@ -0,0 +1,215 @@ + + + + + {data.filtres.q ? `« ${data.filtres.q} » — recherche` : 'Recherche'} · Veille législative + + + + +

Recherche

+ + + +{#if chipsActifs.length} +
+ Filtres actifs : + {#each chipsActifs as chip (chip.champ + chip.valeur)} + + {chip.libelle} + + Retirer ce filtre + + {/each} + + Tout effacer + +
+{/if} + +
+ + +
+

+ {data.resultats.total} + {pluriel(data.resultats.total, 'texte')} + {#if data.filtres.q}pour « {data.filtres.q} »{/if} +

+ + {#if data.resultats.textes.length} +
+ {#each data.resultats.textes as texte (texte.id)} + + {/each} +
+ + {#if data.resultats.pages > 1} + + {/if} + {:else} +
+

Aucun résultat

+

+ Aucun texte ne correspond à cette combinaison de filtres. Essayez de retirer un + critère, ou de chercher sur un mot plus général. +

+ Repartir de zéro +
+ {/if} +
+
+ + diff --git a/web/src/routes/textes/[slug]/+page.server.ts b/web/src/routes/textes/[slug]/+page.server.ts new file mode 100644 index 0000000..636756d --- /dev/null +++ b/web/src/routes/textes/[slug]/+page.server.ts @@ -0,0 +1,19 @@ +import { error } from '@sveltejs/kit'; +import { texte } from '$serveur/requetes'; +import { DATE_ARRETE_CORPUS } from '$serveur/db'; +import type { PageServerLoad } from './$types'; + +/** + * Fiche détaillée — rendu serveur. + * + * Pas de prérendu : la fiche doit être fraîche dès que le pipeline écrit dans + * SQLite, sans reconstruction du site. C'est aussi la page la plus utile au + * référencement, donc celle qui gagne le plus au rendu serveur. + */ +export const load: PageServerLoad = async ({ params }) => { + const fiche = texte(params.slug); + if (!fiche) { + error(404, `Aucun texte suivi ne porte l'identifiant « ${params.slug} ».`); + } + return { texte: fiche, dateArrete: DATE_ARRETE_CORPUS }; +}; diff --git a/web/src/routes/textes/[slug]/+page.svelte b/web/src/routes/textes/[slug]/+page.svelte new file mode 100644 index 0000000..235190d --- /dev/null +++ b/web/src/routes/textes/[slug]/+page.svelte @@ -0,0 +1,571 @@ + + + + {t.titre_court} · Veille législative Gwadloup + + + + + +
+
+ {#if t.numero_officiel}

Loi n° {t.numero_officiel}

{/if} +

{t.titre_court}

+ {#if t.titre_officiel && t.titre_officiel !== t.titre_court} +

{t.titre_officiel}

+ {/if} + +
+ + + {LIBELLES_TYPE[t.type] ?? t.type} + + Confiance : {LIBELLES_CONFIANCE[t.confiance] ?? t.confiance} + +
+ + {#if t.a_verifier === 1 && t.motif_verification} + + {/if} +
+ +
+
+ {#if t.resume} +
+

Résumé

+

{t.resume}

+
+ {/if} + + {#if t.points_cles.length} +
+

Points clés

+
    + {#each t.points_cles as point (point)} +
  • {point}
  • + {/each} +
+
+ {/if} + + {#if Object.keys(t.impacts).length} +
+

Effets sur les libertés

+
+ {#each PUBLICS as pub (pub)} + {@const impact = t.impacts[pub]} + {#if impact} +
+

{pub}

+

{LIBELLES_IMPACT[`${pub}:${impact.sens}`] ?? impact.sens}

+ {#if impact.note}

{impact.note}

{/if} +
+ {/if} + {/each} +
+
+ {/if} + + {#if t.evenements.length} +
+

Parcours du texte

+
    + {#each t.evenements as evenement (evenement.date_evenement + evenement.type_etape)} +
  1. + +
    + {LIBELLES_ETAPE[evenement.type_etape] ?? evenement.type_etape} + {#if evenement.previsionnel === 1} + prévisionnel + {/if} +

    {evenement.description}

    +
    +
  2. + {/each} +
+
+ {/if} + + {#if t.decisions_cc.length} +
+

Conseil constitutionnel

+ {#each t.decisions_cc as decision (decision.numero_affaire)} +
+

Affaire n° {decision.numero_affaire}

+
+ {#if decision.date_saisine} +
Saisine
+
{dateLongue(decision.date_saisine)}
+ {/if} + {#if decision.date_decision} +
Décision
+
{dateLongue(decision.date_decision)}
+ {:else if decision.date_decision_attendue} +
Décision attendue
+
{dateLongue(decision.date_decision_attendue)}
+ {/if} + {#if decision.resultat} +
Résultat
+
{LIBELLES_RESULTAT_CC[decision.resultat] ?? decision.resultat}
+ {/if} + {#if decision.saisissants} +
Saisissants
+
{decision.saisissants}
+ {/if} +
+ {#if decision.resume}

{decision.resume}

{/if} +
+ {/each} +
+ {/if} + +
+

Sources ({t.sources.length})

+

+ Chaque source est citée telle qu'elle a été consultée. Les sources primaires + (Journal officiel, assemblées, Conseil constitutionnel, Conseil d'État) sont + marquées T1, la presse et les analyses T2. +

+
    + {#each t.sources as source (source.url + (source.marqueur ?? ''))} +
  1. +
    + {source.tier} + {#if source.editeur}{source.editeur}{/if} + {#if source.date_publication} + + {/if} +
    + {#if source.titre}

    {source.titre}

    {/if} + {#if source.extrait_verbatim} +
    {source.extrait_verbatim}
    + {/if} + {#if source.contexte}

    {source.contexte}

    {/if} + + {source.url} + +
  2. + {/each} +
+
+
+ + +
+
+ + diff --git a/web/src/service-worker.ts b/web/src/service-worker.ts new file mode 100644 index 0000000..8643147 --- /dev/null +++ b/web/src/service-worker.ts @@ -0,0 +1,121 @@ +/// +/// + +/** + * Service worker minimal — consultation hors ligne des dernières données. + * + * Deux stratégies, et pas une de plus : + * + * - **Le coffre du build** (JS, CSS, polices, icônes) est mis en cache à + * l'installation et servi depuis le cache. Ces fichiers portent une empreinte + * dans leur nom : ils ne changent jamais sous le même nom. + * - **Les pages et l'API** passent par le réseau d'abord, avec repli sur le + * cache. Une veille législative doit montrer l'état le plus récent qu'elle + * peut atteindre ; servir une page périmée alors que le réseau répond serait + * trompeur sur un sujet où une date de promulgation change tout. + * + * Le hors-ligne n'est donc pas un mode de consultation normal : c'est un filet + * pour les connexions instables, fréquentes en zone rurale guadeloupéenne. La + * page servie depuis le cache le signale à l'utilisateur via l'en-tête ajouté + * `X-Depuis-Cache`. + */ +import { build, files, version } from '$service-worker'; + +const sw = self as unknown as ServiceWorkerGlobalScope; + +const CACHE_COFFRE = `coffre-${version}`; +const CACHE_PAGES = `pages-${version}`; + +// Les polices et le manifeste valent d'être précachés ; pas les gros binaires. +const A_PRECACHER = [...build, ...files.filter((f) => !f.endsWith('.map'))]; + +sw.addEventListener('install', (evenement) => { + evenement.waitUntil( + caches.open(CACHE_COFFRE).then((cache) => cache.addAll(A_PRECACHER)) + ); + // Le nouveau worker prend la main sans attendre la fermeture des onglets : + // une correction de données ne doit pas rester bloquée derrière un onglet + // ouvert depuis une semaine. + sw.skipWaiting(); +}); + +sw.addEventListener('activate', (evenement) => { + evenement.waitUntil( + (async () => { + const noms = await caches.keys(); + await Promise.all( + noms + .filter((nom) => nom !== CACHE_COFFRE && nom !== CACHE_PAGES) + .map((nom) => caches.delete(nom)) + ); + await sw.clients.claim(); + })() + ); +}); + +sw.addEventListener('fetch', (evenement) => { + const requete = evenement.request; + if (requete.method !== 'GET') return; + + const url = new URL(requete.url); + // Rien qui ne vienne de cette origine : la politique de sécurité de contenu + // interdit déjà les tiers, le worker ne les mettra pas en cache non plus. + if (url.origin !== location.origin) return; + + // Coffre du build : cache d'abord, sans condition. + if (A_PRECACHER.includes(url.pathname)) { + evenement.respondWith( + caches + .open(CACHE_COFFRE) + .then((cache) => cache.match(url.pathname)) + .then((reponse) => reponse ?? fetch(requete)) + ); + return; + } + + // Pages et API : réseau d'abord, cache en repli. + evenement.respondWith(reseauPuisCache(requete)); +}); + +async function reseauPuisCache(requete: Request): Promise { + const cache = await caches.open(CACHE_PAGES); + + try { + const reponse = await fetch(requete); + if (reponse.ok) { + cache.put(requete, reponse.clone()); + } + return reponse; + } catch { + const enCache = await cache.match(requete); + if (enCache) { + // On signale explicitement que la réponse peut être périmée. + const entetes = new Headers(enCache.headers); + entetes.set('X-Depuis-Cache', 'true'); + return new Response(enCache.body, { + status: enCache.status, + statusText: enCache.statusText, + headers: entetes + }); + } + + return new Response( + ` + Hors ligne +
+

Pa ni rézo

+

Cette page n'a pas encore été consultée et le réseau est indisponible. + Les pages déjà visitées restent accessibles.

+
+
`, + { status: 503, headers: { 'content-type': 'text/html; charset=utf-8' } } + ); + } +} diff --git a/web/static/favicon.svg b/web/static/favicon.svg new file mode 100644 index 0000000..8ee25d4 --- /dev/null +++ b/web/static/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/static/manifest.webmanifest b/web/static/manifest.webmanifest new file mode 100644 index 0000000..b503af1 --- /dev/null +++ b/web/static/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "Veille législative Gwadloup", + "short_name": "Vèy Lwa", + "description": "Lois françaises et effets sur les libertés des individus, des associations et des entreprises — zoom Guadeloupe.", + "lang": "fr", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait-primary", + "background_color": "#0D0D0D", + "theme_color": "#0D0D0D", + "icons": [ + { "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }, + { "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "maskable" } + ], + "categories": ["news", "government", "reference"] +} diff --git a/web/static/theme.js b/web/static/theme.js new file mode 100644 index 0000000..df30348 --- /dev/null +++ b/web/static/theme.js @@ -0,0 +1,19 @@ +/** + * Applique le thème choisi avant le premier rendu. + * + * Le thème sombre est l'identité par défaut : on n'ajoute la classe que si le + * visiteur a explicitement demandé le clair, ou si son système le préfère sans + * qu'il ait exprimé de choix. + */ +(function () { + try { + var choix = localStorage.getItem('theme'); + var systemeClair = + window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches; + if (choix === 'clair' || (choix === null && systemeClair)) { + document.documentElement.classList.add('light-theme'); + } + } catch (e) { + /* stockage indisponible : on garde le thème sombre par défaut */ + } +})(); diff --git a/web/svelte.config.js b/web/svelte.config.js new file mode 100644 index 0000000..43df6fa --- /dev/null +++ b/web/svelte.config.js @@ -0,0 +1,39 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** + * Architecture de rendu : « transitional app » en rendu serveur sur + * adapter-node, avec prérendu ciblé des seules pages figées. + * + * Le rendu serveur est le défaut partout : les fiches doivent être fraîches dès + * que le pipeline écrit dans SQLite, et les facettes de recherche vivent dans + * `url.searchParams`, ce que le prérendu interdit. + * + * `precompress` génère les variantes .gz et .br au build ; la négociation est + * faite par le reverse proxy. + */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ precompress: true }), + alias: { + $composants: 'src/lib/composants', + $serveur: 'src/lib/server' + }, + csp: { + // Aucune ressource tierce : ni police, ni script, ni image externe. + directives: { + 'default-src': ['self'], + 'img-src': ['self', 'data:'], + 'style-src': ['self', 'unsafe-inline'], + 'font-src': ['self'], + 'connect-src': ['self'], + 'frame-ancestors': ['none'], + 'base-uri': ['self'], + 'form-action': ['self'] + } + } + } +}; + +export default config; diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..a8f10c8 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..227c50d --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,9 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + // better-sqlite3 est un module natif : il ne doit jamais être empaqueté. + ssr: { external: ['better-sqlite3'] }, + build: { target: 'es2022' } +});