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>
Étape 0 de la refonte pipeline crawl/géocodage.
Auto-requeue du géocodage
- migration 013: trigger trg_bands_geocode_dirty (AFTER UPDATE OF location_text
ON bands) supprime les band_locations d'un band dès que sa localisation
change réellement (IS DISTINCT FROM) — plus de points fantômes après un
déménagement détecté par le crawler incrémental.
- enqueue.py: en plus du déclenchement manuel, un scan automatique tourne
toutes les ENQUEUE_AUTO_INTERVAL_MIN minutes (défaut 60) et rattrape les
bands rendus "dirty" par le trigger (idempotent, ON CONFLICT DO NOTHING —
pas de canal de notification supplémentaire nécessaire). Chaque exécution
(manuelle ou auto) crée une ligne crawl_run + logs rattachés, en miroir de
crawler/src/db.py, pour apparaître dans l'activité admin à venir.
Crawl complet en calcul glissant
- remplace CRAWLER_SCHED_FULL_DAY (jour fixe du mois) par
CRAWLER_FULL_CRAWL_INTERVAL_DAYS (défaut 60) basé sur le checkpoint
last_full_crawl_at — robuste au calendrier et aux redémarrages (vérifié
aussi au démarrage, pas seulement le tick quotidien 03:00 UTC).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Geocoder → Geoapify (bcb790b9007644d1a44ff2391118479c) :
- Remplace Nominatim par Geoapify dans apps/geocoder/src/worker.py
- Délai réduit à 0.22s (5 req/s vs 1 req/s Nominatim) → bien plus rapide
- Même logique de cache geocode_cache, même fallback backoff progressif
- Env vars : GEOAPIFY_API_KEY (obligatoire), GEOCODER_MIN_DELAY/JITTER
Fix crawler incremental_modified (Expecting value: line 1 column 1) :
- ma_http.get_json() retenait sans retry sur JSONDecodeError (corps vide =
session Chrome morte). Désormais : refresh session + retry, comme pour
les erreurs 403/429.
Admin dashboard :
- Bands : colonne Lieu, champ recherche lieu séparé (location_q), filtres
"Géocodé oui/non" (has_lat) et "Lieu vide/renseigné" (has_location)
- Queue : bouton "Annuler runs bloqués >30min" (POST /admin/api/crawl-runs/
cleanup), auto-refresh 15s, colonne Progression avec durée elapsed pour
les runs actifs
- Dashboard : toutes les listes pays/genre/statut sans limite (scroll interne)
- Progression live : crawler écrit les stats dans crawl_run toutes les 50
bands (update_crawl_run_progress), visible dans Queue en temps réel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nouveau service apps/admin (admin.metalfrom.eu / admin.dev.metalfrom.eu) :
- Frontend statique vanilla JS/CSS reprenant le design system du site
(login, dashboard stats, table bands éditable, queue d'enrichissement,
historique crawl_run, logs live, checkpoints, journal d'audit)
- nginx reverse-proxy /admin/api/* et /admin/auth/* vers le service api
interne (same-origin côté navigateur, pas de CORS cross-site nécessaire
pour le cookie de session)
apps/api :
- Nouvelle auth dédiée au dashboard, séparée du token BM_IMPORT_TOKEN
existant : login bcrypt + session JWT en cookie httpOnly/secure/
sameSite=strict, rate-limit + lockout après 5 échecs/15min, seeding
du compte admin via env vars (jamais de mot de passe en clair en DB
ou en git)
- Routes /admin/api/* : stats, queue (breakdown priorité identique au
crawler Python), bands (recherche/tri/pagination/édition + audit log),
crawl-runs, crawl-checkpoints, logs, audit-log
- trustProxy activé (Traefik + nginx en amont)
apps/crawler :
- log_event() écrit dans la nouvelle table crawl_log (run start/finish/
erreurs) pour que le dashboard affiche les logs sans exposer le socket
Docker (choix délibéré : pas de docker.sock monté, accès DB only)
migration 006_admin_dashboard.sql : admin_users, admin_login_attempts,
admin_audit_log, crawl_log
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug latent : la fonction n'était jamais appelée avant car get_bands_to_enrich
retournait 0. Exposé dès le premier run réel avec la nouvelle priority queue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BAND_MIN_DELAY: 2.5→1.5s, BAND_MAX_DELAY: 5.0→2.5s (avg 2s/band)
- ENRICH_LIMIT: 200→500 (configurable via CRAWLER_ENRICH_LIMIT)
- get_bands_to_enrich: remplace la queue FIFO simple par une file de
priorité à 4 niveaux :
1. Nouveaux bands (band_page absent)
2. Modifiés depuis dernier enrichissement (updated_at > crawled_at + 1min)
3. Héritage ancien scraper (crawled_at IS NULL, band_page présent)
4. Stale (crawled_at < now() - 30 days)
- Suppression de get_bands_stale() (logique absorbée par la queue)
Objectif : ~17 jours pour réenrichir les 103k bands
(6000 bands/jour à raison de 500/run × 12 runs/24h)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
L'endpoint /archives/ajax-band-list retourne 6 colonnes :
[date, band_link, country_link, genre, time, user]
et non 4 comme supposé initialement. Le band_link est à row[1],
country à row[2], genre à row[3].
Les dates MA n'incluent pas l'année (ex: "June 1") donc date_str=None :
la logique d'arrêt incrémental sur date est désactivée, on re-fetch
les ~800 entrées récentes à chaque run (idempotent, coût négligeable).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- flaresolverr.py: refactor stateless (create_session/destroy_session/get)
L'ancienne approche warmup→cookies→requests était bloquée par Cloudflare
(JA3 TLS fingerprinting). Toutes les requêtes passent maintenant via une
session Chrome persistante FlareSolverr.
- ma_http.py: MASession utilise les sessions FS persistantes. Ajout de
_extract_json (html.unescape + extraction <pre>) et _build_url.
Gestion du refresh sur 403/429 avec recréation de session.
- db.py: ajout du champ data JSONB dans upsert_bands INSERT+ON CONFLICT.
Avant ce fix, l'URL n'était jamais stockée → get_bands_to_enrich retournait
toujours 0 résultats → enrichissement mort.
- scraper_band.py: schéma band_page aligné sur l'historique DB (lineup
structuré avec URLs artistes, discography_url, lyrical_themes, location,
info_raw). Ajout de "Themes" dans _pick pour couvrir les deux variantes
de clé HTML (MA utilise parfois "Themes", parfois "Lyrical themes").
- 005_fix_checkpoints_and_data.sql: renomme last_additions_check →
last_created_check (clé morte vs clé utilisée par le code). Backfill
enriched=true + colonnes top-level (formed_year, themes, status,
location_text) depuis band_page JSONB pour les 85k bands de l'ancien scraper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- apps/crawler/ : service Python complet, remplace les scripts locaux
- FlareSolverr pour bypasser Cloudflare (cookies CF → session requests)
- Crawl incrémental : /archives/band-list/by/created et /by/modified
- Crawl complet Europe : pagination AJAX /browse/ajax-country/
- Enrichissement : pages individuelles de bands (themes, membres, label, hash)
- Écriture directe en DB (upserts bulk, idempotents)
- Scheduler intégré (schedule library) : incrémental 4h, enrich 2h, full le 1er du mois
- Tracking via crawl_run et crawl_checkpoint (migration 004)
- docker-compose.dev.yml : flaresolverr + crawler ajoutés
Full crawl désactivé en dev (CRAWLER_SCHED_FULL_DAY=0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>