Commit graph

11 commits

Author SHA1 Message Date
Nicolas FRYDER
fcb20e2e11 perf(api): compression des réponses, et troncature de la carte enfin signalée
Le site public charge l'intégralité du corpus une seconde après l'ouverture de
la page (loadAllHeatPoints), pour la heatmap et la recherche. Rien ne
compressait dans la chaîne — ni Fastify, ni Traefik. Ce n'était donc pas un
risque théorique mais la facture de chaque visiteur, à chaque visite.

Mesuré sur 100 000 groupes avec des données réalistes, en-tête Accept-Encoding
d'un vrai navigateur : 21,5 Mo deviennent 1,9 Mo.

gzip est préféré à brotli, contre l'intuition : br sort 2,3 Mo là où gzip fait
1,9 Mo. La qualité brotli par défaut est réglée pour la vitesse, et sur ce JSON
très répétitif elle perd sur les deux tableaux à la fois — taille ET CPU. Seuil
à 1 Ko, en dessous duquel compresser coûte plus que ça ne rapporte.

/api/clusters plafonnait par ailleurs sa réponse à 2000 groupes ou 1000
clusters sans le dire, ni côté API ni côté client : dans une zone dense, des
groupes disparaissaient de la carte sans aucun signal. La réponse porte
désormais `truncated` et `limit`, et l'interface affiche un avertissement
invitant à zoomer. Clé de traduction ajoutée dans les 21 langues, la
convention du fichier étant une couverture complète.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:35:12 +02:00
Nicolas FRYDER
1fd0c8933b fix(a11y): les modales du site public déclaraient aria-modal sans piéger le focus
Les deux modales (groupes sans coordonnées, mentions légales / FAQ) portent
role="dialog" et aria-modal="true" — ce qui affirme aux lecteurs d'écran que
tout le reste de la page est inerte. C'était faux : aucune gestion du focus.

Concrètement, à l'ouverture le focus restait derrière le voile, la tabulation
ressortait sur des commandes invisibles, et la fermeture ne rendait pas le
focus à son point de départ. Un utilisateur au clavier se retrouvait perdu
dans une page dont on venait de lui dire qu'elle était inaccessible.

Le dashboard admin faisait déjà tout cela correctement ; on porte le même
traitement, factorisé en openBackdrop / closeBackdrop puisque les modales du
site public sont des éléments statiques que l'on bascule, et non créés à la
volée comme côté admin.

Non couvert par un test : le site public n'a aucun harnais de bout en bout —
les 89 parcours Playwright ne visent que l'admin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 13:57:58 +02:00
Nicolas FRYDER
582fdc5451 fix(web): la carte se vidait en silence dès qu'on dézoomait au niveau monde
Le viewport est élargi de 20 % pour précharger les bords, sans aucun bornage.
Au-delà d'un certain dézoom, la bbox sortait des limites géographiques :

  zoom 2 (monde) -> -164,-88.4,144,110.4

/api/clusters rejette en 400 toute coordonnée hors [-90,90] / [-180,180].
Vérifié contre la vraie validation : les zooms 3 et 4 passent, les zooms 1 et 2
renvoient « Coordinates out of range ». L'utilisateur qui dézoomait ne voyait
donc plus aucun point, sans le moindre message.

Leaflet renvoie par ailleurs des longitudes au-delà de ±180 quand la vue
chevauche plusieurs copies du monde ; le bornage seul produirait alors un
intervalle vide, que l'API rejette aussi. On retombe dans ce cas sur la bande
complète, que la vue couvre de toute façon.

Le calcul part dans pure.js, conformément à la convention du fichier, avec des
cas limites et une propriété fast-check : aucune vue Leaflet imaginable ne doit
produire une bbox que l'API refuse. Vérifié que cette propriété échoue bien sur
l'ancienne implémentation (contre-exemple : east = 180.00000000000003).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 13:55:54 +02:00
Nicolas FRYDER
1fe5b4a618 fix(web): SRI sur les dépendances CDN et timeout des appels API
Les cinq bibliothèques chargées depuis unpkg et jsDelivr (Leaflet,
MarkerCluster, leaflet.heat, noUiSlider, flag-icons) n'avaient aucun contrôle
d'intégrité : un CDN compromis exécutait du code arbitraire sur le site public.
Les neuf hashes ont été recalculés depuis les URL versionnées.

GoatCounter reste sans SRI : son script n'est pas versionné.

apiGet() reçoit un timeout : sans lui, une API qui ne répond jamais laissait
l'interface et le rechargement du viewport bloqués indéfiniment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:44:26 +02:00
60074fb015 feat(qualité): outillage de test complet, CI locale, annulation réelle des runs
Some checks are pending
CI / javascript (push) Waiting to run
CI / python (push) Waiting to run
CI / mutation (push) Waiting to run
Le dépôt n'avait aucun test, aucun linter, aucune vérification de types.

Outillage
- ESLint 9 (flat config) sur api + les deux frontends, Ruff sur le Python
- tsc --checkJs sur l'API (pas de TypeScript, juste la vérification)
- Vitest : 401 tests JS ; pytest : 43 tests Python
- Tests de mutation (Stryker), deux profils : logique pure et API complète
- Hook pre-push `npm run check` (~17 s) — le déploiement Coolify est sur webhook,
  c'est donc la seule porte de qualité avant la mise en ligne
- Workflow Forgejo Actions prêt (inerte tant qu'aucun runner n'est enregistré)

Sécurité
- Injection SQL authentifiée dans resolve-conflict : `field` était interpolé
  dans le SET sans allowlist
- timingSafeEqual levait sur un jeton multi-octets (500 au lieu de 401)
- setErrorHandler écrasait tous les 4xx en 500
- .env.example : ADMIN_JWT_SECRET et ADMIN_SEED_* n'étaient documentés nulle part
  alors que leur absence casse toute connexion admin

Annulation réelle des crawl_run (migration 014)
- L'API posait status='error' sans que le crawler en sache rien : le process
  continuait, et son UPDATE final ne matchait plus (run réussi affiché en erreur)
- Protocole coopératif : drapeau cancel_requested lu à chaque lot, le crawler
  écrit lui-même status='cancelled'

Cohérence géographique (migration 014)
- Le trigger 013 supprimait les band_locations sans purger le point dénormalisé
- L'édition admin de lat/lon n'atteignait jamais band_locations : la carte
  ignorait la correction. Override step_order = -1, dans une transaction

Corrections
- limit/offset NaN → 500 au lieu de 400
- OPTIONS sans `return reply` (Fastify poursuivait le cycle de vie)
- listen() sans catch, cast ::text en dur sur les colonnes numériques
- /admin/api/logs ne renvoyait pas sa pagination
- a11y : sélecteur de langue annoncé comme liste vide (role=option manquant)

Nettoyage
- apps/web/quizz-site supprimé (sans rapport avec le projet)
- Code mort : openModal(), LANG_NAMES, double import, variables inutilisées
- .dockerignore ajoutés ; node_modules racine n'était pas gitignoré

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 10:05:40 +02:00
39e6b2a66f feat(map): carte multi-localisations — un point par step géocodé
La carte clusterisait depuis bands.geom (un seul point par groupe). Désormais
/api/clusters source band_locations : un groupe multi-périodes (ex. Thessaloniki
puis Boston) apparaît comme plusieurs points distincts.

- migration 010: colonne geom générée (Point,4326) + index GIST sur
  band_locations (évite le seq-scan sur les requêtes bbox)
- /api/clusters: FROM band_locations bl JOIN bands b, bbox via geom && envelope,
  filtres (pays/statut/genre/année) sur b.*, points issus de bl.lat/lon ;
  la JSON des clusters porte location_raw + step_label
- web parseBandData: expose location_raw + step_label (base pour différencier
  plus tard un groupe qui a déménagé d'un groupe resté sur place)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:52:40 +02:00
3d3362fea0 feat(i18n): +10 langues (hu, da, fi, sk, hr, sl, lt, lv, et, nb)
Magyar, Dansk, Suomi, Slovenčina, Hrvatski, Slovenščina, Lietuvių, Latviešu,
Eesti, Norsk bokmål. 21 langues au total.

- locales.js: traductions complètes des ~50 clés (faq, legal inclus)
- index.html: boutons dropdown avec flag-icons (sl→si, et→ee, nb→no, da→dk)
- app.js: SUPPORTED_LANGS, LANG_FLAGS, LANG_NAMES

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:47:36 +02:00
68e483762e feat: flag-icons dropdown + admin live monitor
- Sélecteur de langue : dropdown custom avec flag-icons CDN (fi fi-xx), fermeture click-outside, aria-expanded
- Admin Monitor : nouvelle vue live avec status temps réel (crawl runs, géocodeur, jobs en attente)
- Log stream incrémental : fetch uniquement les nouveaux logs via min_id, buffer 500 entrées, filtre level + recherche texte client-side, flash animation sur nouvelles lignes
- Boutons inline Annuler pour runs actifs et jobs en attente
- API : GET /admin/api/live (agrégé), POST crawl-runs/:id/cancel, POST job-triggers/:id/cancel, param min_id sur GET /logs
- Refresh configurable 3/5/10/30s, pause/reprendre, vider le buffer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 21:02:13 +02:00
dc9239c38e fix(i18n): sélecteur langue sans emoji flags, détection navigateur multi-lang, défaut anglais
- Options du select : noms natifs (Français, English, Deutsch…) à la place des emojis flags
- detectLang() itère navigator.languages[] pour trouver la première langue supportée
- Fallback sur "en" au lieu de "fr" si aucune langue connue
- Fallback t() sur "en" au lieu de "fr"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 20:51:39 +02:00
6a1b2c3e44 feat: i18n front-end (11 langues), centre de commandes admin, remerciements légaux
- i18n front-end (fr/en/de/es/it/pl/nl/ro/pt/cs/sv) via locales.js + data-i18n attrs
- Sélecteur de langue avec drapeaux dans la topbar, persisté en localStorage
- Mentions légales et FAQ générées dynamiquement par locale (buildLegal/buildFaq)
- Section "Remerciements" : Geoapify, OSM, Leaflet, Metal Archives, HellBlazer
- Admin : centre de commandes (crawler jobs, géocodeur, maintenance, live log tail 10s)
- Admin : boutons reset-errors / requeue-all géocodeur avec confirmation
- Admin : toutes les actions admin loguées dans crawl_log pour audit
- Suppression anciens artefacts (worker, infra/, apps/api/src/index.js)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 20:41:41 +02:00
d4e1bf2e15 chore: initial commit — snapshot VPS 2026-06-27
- apps/api      : API REST Node.js/Fastify + PostgreSQL/PostGIS
- apps/geocoder : Worker Python géocodage Nominatim
- apps/worker   : Scraper Python Metal-Archives
- apps/web      : Frontend statique Leaflet/clustering/heatmap
- infra/        : docker-compose + init.sql

Déployé via Coolify + Traefik sur VPS OVH.
2026-06-27 08:53:39 +00:00