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>
bands.ma_id est BIGINT (crawler génère jusqu'à ~3.5e9) mais band_locations.ma_id
avait été créé en INTEGER (max 2.1e9). Tout INSERT pour un band à ma_id élevé
échouait en "integer out of range", erreur avalée silencieusement par l'enqueue.
62 239 bands (sur 96 143 localisables) n'entraient donc jamais dans le pipeline,
d'où 0 llm_needed / 0 erreur trompeurs.
- migration 008: ma_id BIGINT (installs neuves)
- migration 009: ALTER COLUMN ma_id TYPE BIGINT (DB existante)
- enqueue.py: compte les inserts échoués (failed=) au lieu de les avaler
- worker.py: sync_bands_primary prend le lieu d'ORIGINE (step 0) et non la
dernière localisation — garde les groupes européens en Europe
- admin app.js: coût LLM robuste au NaN (NUMERIC accepte la valeur spéciale NaN)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- enqueue.py devient un daemon qui poll job_triggers (type geocoder_enqueue)
toutes les 15s — plus de commande manuelle à lancer
- API : geocoder_enqueue ajouté aux job types autorisés
- Admin UI page Géocodage : bouton "Lancer l'enqueue" (vert)
- Admin UI Centre de commandes : même bouton dans la section Géocodeur
- docker-compose : geocoder-enqueue passe à restart: unless-stopped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- 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>
- 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>
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>
Root cause du 404 intermittent : les stacks prod et dev déclarent tous les
deux un service nommé "api" attaché au même réseau externe coolify. Docker
DNS répond aléatoirement avec l'IP du conteneur prod OU dev pour le nom
"api" — quand admin/nginx tombait sur le mauvais conteneur (inaccessible
depuis cet environnement), ça donnait "connect() failed: Connection
refused" puis 404 côté client. Le resolver dynamique (commit précédent)
ne corrige pas cette collision, il la rend juste plus visible/intermittente.
Fix : alias réseau unique par environnement (bm-api-internal en prod,
dev-api-internal en dev) sur le service api, et apps/admin/Dockerfile
prend un ARG API_UPSTREAM substitué par sed dans nginx.conf au build,
pointant vers l'alias correspondant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nginx résolvait "api" une seule fois au démarrage et gardait l'IP en
cache indéfiniment (comportement par défaut pour un proxy_pass avec
hostname statique). Après un redeploy Coolify du service api (nouveau
conteneur = nouvelle IP), nginx continuait à taper sur l'ancienne IP
morte -> "connect() failed (111: Connection refused)" -> 404 côté
client. Fix : resolver 127.0.0.11 (DNS Docker embarqué) + proxy_pass
via variable pour forcer une résolution à chaque requête.
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>
pg.Client ne peut pas être réutilisé après un connect() échoué.
Le retry créait le même client → "Client has already been connected"
dès l'attempt 2. On crée maintenant un nouveau client à chaque essai.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
migrate.js now retries up to 12 times with linear backoff (max 30s per attempt)
instead of crashing immediately — prevents container restart loops when the
standalone Coolify DB is temporarily unreachable at startup.
CORS_ORIGINS in docker-compose.dev.yml now uses ${CORS_ORIGINS:-https://dev.metalfrom.eu}
to match the prod pattern and allow Coolify UI override.
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>
- Ajoute geocoder-worker et worker sur le réseau coolify pour
résoudre le hostname DB (lgrep99...) qui n'est accessible que
depuis le réseau coolify
- CORS_ORIGINS env var optionnel pour configurer les origines
autorisées (utilisé par l'env dev)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DB PostgreSQL extraite du compose (geree par Coolify avec backup R2)
- DATABASE_URL devient une variable externe injectee par Coolify
- Ajout apps/web/Dockerfile (nginx alpine servant le site statique)
- Labels Traefik: websecure->https, le->letsencrypt, reseau coolify
- Suppression code-server (remplace par workflow git)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>