Commit graph

20 commits

Author SHA1 Message Date
43d6527172 fix: sync du point principal + verrou de migration, redis mort retiré, tests par propriétés
Some checks are pending
CI / javascript (push) Waiting to run
CI / python (push) Waiting to run
CI / mutation (push) Waiting to run
Passe critique sur les zones non vérifiées.

BUG — la correction manuelle d'un lieu n'atteignait pas la moitié du site
bands.lat/lon/geom est une dénormalisation du lieu d'origine, maintenue par
sync_bands_primary() dans geocoder/worker.py. La saisie manuelle de
coordonnées (PATCH /admin/api/locations/:id, ajoutée récemment) écrivait
band_locations sans jamais la déclencher : la correction apparaissait sur la
carte — qui lit band_locations — mais jamais dans la liste, les statistiques
ni la heatmap, qui lisent bands. Le groupe restait affiché au mauvais endroit
indéfiniment.
La synchronisation est répliquée en SQL dans la même transaction, avec la même
règle de tri (step_order ASC, id ASC) que le worker Python.

BUG — course entre réplicas au démarrage (migrate.js)
Le script s'exécute au démarrage de CHAQUE conteneur API. Deux réplicas
démarrant ensemble lisaient tous deux schema_migrations vide et appliquaient
les mêmes fichiers en parallèle : au mieux une violation de clé primaire qui
faisait échouer le démarrage, au pire deux ALTER concurrents.
Verrou consultatif pg_advisory_lock, relâché explicitement. migrate.js
n'exécute plus au chargement s'il est importé (nécessaire pour le tester).

CODE MORT — redis
Signalé au tout début, jamais retiré : un conteneur redis + un volume
persistant dans les DEUX composes, sans une seule référence dans le code.

TESTS AJOUTÉS

- Intégration migrations (7 tests) : application sur base vierge, rejouabilité,
  ordre lexicographique, relâchement du verrou, échec bruyant sur migration
  invalide, et surtout DEUX MIGRATIONS SIMULTANÉES sur une base vierge —
  le cas qui motivait le verrou.

- Tests par PROPRIÉTÉS (fast-check, 21 tests) : batterie qui manquait.
  Les tests par l'exemple ne couvrent que les cas auxquels on a pensé.
  L'invariant central : toute entrée arbitraire produit soit une valeur
  normalisée valide, soit une ValidationError — jamais une autre exception,
  jamais NaN. C'est ce qui garantit un 400 plutôt qu'un 500. Vérifie aussi
  la cohérence offset = (page-1) × pageSize, le domaine des coordonnées,
  et qu'aucun caractère de contrôle ne survit à la validation.

Tests : 601 JS + 61 intégration, 63 Python, 89 Playwright

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 21:03:17 +02:00
1ae5aa4b57 feat(geocoder): seuil de confiance strict, dedup, purge ancien pipeline
É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>
2026-07-02 21:07:16 +02:00
72a52a8d3b feat(geocoder): seuil de confiance strict, dedup, purge ancien pipeline
Durcissement fiabilité (suite audit).

Confiance & granularité
- worker: un résultat n'est accepté ('done') que si confiance >= GEOCODE_MIN_CONFIDENCE
  (0.7) ET granularité non-grossière (rejette country/state/county/region). Sinon
  on continue les fallbacks, puis -> llm_needed.
- fix majeur: sur cache hit la confiance était écrite 0.5 en dur (97% des lignes
  faussées). Elle est désormais lue depuis geocode_cache (recalculée du raw).
- migration 012: colonnes confidence+granularity sur geocode_cache (recalcul des
  entrées Geoapify depuis le raw), geocode_granularity sur band_locations.

Dedup (7x moins de travail)
- fast-path: un band_location dont le (lieu,pays) est déjà 'done' copie le
  résultat sans appel API. Index fonctionnel lower(location_raw).

Ancien pipeline retiré
- geocode_queue n'est plus lu nulle part (endpoints /geocoding/reset-errors et
  /requeue-all supprimés, /live et /geocoding et Monitor basculés sur
  band_locations, panneau admin "ancien pipeline" retiré).

Boutons reset (onglet Géocodage, zone dangereuse)
- POST /admin/api/locations/reset-all: remet tout en queue + efface coords
- POST /admin/api/geocode-cache/purge-nominatim: purge le cache Nominatim

Divers
- groq_worker: coût calculé par modèle (70B vs 8B) au lieu du tarif 70B fixe
- GEOCODE_MIN_CONFIDENCE ajouté aux deux compose

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:32:16 +02:00
dba6b9f730 feat(geocoding): boutons UI pour déclencher l'enqueue band_locations
- 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>
2026-07-01 21:55:40 +02:00
c39a8513f8 feat(geocoding): pipeline multi-étapes avec LLM Groq free tier
- Migration 008 : tables band_locations (N steps × M villes par band)
  et llm_cache (évite les double-appels LLM par sha256)
- parser.py : parse location_text en steps structurés, gère N/A/Unknown,
  villes multiples (Bergen / Oslo), hiérarchies admin, codes pays
- enqueue.py rewrite : peuple band_locations depuis bands, résout les
  is_country_only avec centroïdes hardcodés (confidence=0.1)
- worker.py rewrite : fallbacks progressifs Geoapify (plus spécifique
  → plus vague), sync bands.lat/lon depuis le step le plus récent,
  bascule en llm_needed après 3 échecs
- groq_worker.py (nouveau) : Groq free tier JSON mode, llm_cache,
  rate-limit par modèle, backoff exponentiel, fallback 8B si 70B saturé
- docker-compose : geocoder-enqueue (one-shot), groq-worker (continu),
  geocoder-worker devient unless-stopped
- Admin API : /geocoding retourne stats band_locations + llm_cache ;
  nouvelles routes /locations/reset-errors /reset-llm /requeue-all
- Admin UI : page Géocodage affiche les deux pipelines en parallèle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 21:49:22 +02:00
3dc483bc4f feat: geocoder Geoapify + fixes crawler + améliorations admin dashboard
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>
2026-06-30 22:44:28 +02:00
10ab0055d5 fix(admin): alias réseau dédié pour éviter la collision DNS prod/dev sur "api"
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>
2026-06-30 22:27:47 +02:00
a7d7bc94de feat(admin): dashboard admin complet (auth forte, API, monitoring)
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>
2026-06-30 22:15:13 +02:00
3230ba8d9a fix(network): remettre coolify external network + compose_parsing_version=5
Root cause identifié: compose_parsing_version=1 (DEV) vs 5 (PROD).
La v1 stripe coolify: external: true → crée uuid_coolify au lieu du vrai réseau.
La v5 préserve correctement l'external network declaration.

Fix appliqué:
- DB Coolify: compose_parsing_version mis à 5 pour l'app DEV
- Tous services sur networks: [coolify] explicitement
- Section networks: coolify external: true, name: coolify dans les deux compose files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 16:44:09 +02:00
777d95fc79 refactor: supprimer toutes les déclarations réseau custom des compose files
Coolify gère le réseau Docker de manière autonome :
- Crée un réseau projet UUID pour la communication inter-services
- Connecte automatiquement les services avec labels Traefik (même enable=false)
  au vrai réseau coolify via docker network connect après le démarrage

Déclarer coolify: external: true dans le compose causait un bug Coolify 4.1.2
qui strippait la config en coolify: null → créait aec2c803..._coolify au lieu
du vrai réseau coolify.

Suppression complète de bm_internal et coolify des deux compose files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 16:35:54 +02:00
c55366fb25 fix: migrate.js retry DB connection + align CORS_ORIGINS pattern on dev
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>
2026-06-27 16:12:10 +02:00
bf03647de6 fix(dev): revenir au standalone Coolify DB + traefik.enable=false
Coolify ne connecte au vrai reseau coolify que les services avec
labels Traefik. Fix: ajouter traefik.enable=false sur les services
sans exposition HTTP (crawler, geocoder-worker, flaresolverr) pour
forcer Coolify a les mettre sur le bon reseau.

DATABASE_URL revient a pointer vers la DB standalone Coolify.
Supprime la DB dans le compose (bm_dev_db_data) - retour a
l architecture standalone correcte.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 16:00:03 +02:00
bc06910708 fix(dev): DB dans le compose (plus de dependance reseau Coolify)
Coolify prefixe les reseaux externes avec l UUID du projet, donc
external: true, name: coolify ne fonctionne pas pour les services
sans labels Traefik. Solution : integrer la DB directement dans
le compose sur bm_internal.

- Service db: postgis/postgis:16-3.4, volume bm_dev_db_data
- Tous les DATABASE_URL passent par postgres://bm:@db:5432/bm
- depends_on db (condition: service_healthy) sur tous les consumers
- crawler, geocoder, flaresolverr: plus de reseau coolify (inutile)
- La DB Coolify standalone n9dttfjalj5trqsg3o8twduo peut etre arretee

Env vars requises dans Coolify UI: POSTGRES_PASSWORD, BM_IMPORT_TOKEN,
PGADMIN_EMAIL, PGADMIN_PASSWORD, NOMINATIM_EMAIL, NOMINATIM_USER_AGENT

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 15:34:25 +02:00
ac7004ca4c fix(dev): ajouter reseau coolify au crawler et geocoder-worker
Sans ce reseau, les containers ne peuvent pas resoudre le hostname
de la DB Coolify (n9dttfjalj5trqsg3o8twduo).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 15:26:08 +02:00
260200e251 feat(crawler): nouveau service crawler server-side via FlareSolverr
- 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>
2026-06-27 15:15:29 +02:00
91166668a6 fix(dev): stabiliser les workers et corriger les domaines
- Supprime le service worker (Playwright, sera réécrit)
- geocoder-worker: restart: no pour éviter la boucle de crash
- Domaines dev-api et dev-pgadmin migrés vers metalfrom.eu
- API_BASE aligné sur dev-api.metalfrom.eu

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 15:04:52 +02:00
d3cb6e64e7 fix(dev): change dev API domain to dev-api.metalfrom.eu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:39:49 +02:00
781382dc65 feat: API_BASE configurable par build arg + DB dev
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:30:08 +02:00
6c00ee2251 fix: docker-compose.dev.yml standalone (not overlay) pour Coolify
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 12:52:11 +02:00
af84da84da fix: revenir aux labels Traefik hardcodés + docker-compose.dev.yml
Coolify ne développe pas \${VAR:-default} dans les labels Docker.
Labels prod: bm-api, bm-web, bm-pgadmin (inchangés).
docker-compose.dev.yml: override pour l'env dev avec les labels
dev-api, dev-web, dev-pgadmin et CORS_ORIGINS dev.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 12:49:28 +02:00