From f145188d1f9b03558d968e4e1bd1ab32d9b878f1 Mon Sep 17 00:00:00 2001 From: Nicolas Fryder Date: Thu, 2 Jul 2026 19:47:30 +0200 Subject: [PATCH] =?UTF-8?q?fix(geocoder):=20band=5Flocations.ma=5Fid=20en?= =?UTF-8?q?=20BIGINT=20=E2=80=94=20d=C3=A9bloque=2062k=20bands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/admin/site/app.js | 3 ++- apps/api/migrations/008_band_locations.sql | 2 +- .../api/migrations/009_band_locations_bigint.sql | 9 +++++++++ apps/geocoder/src/enqueue.py | 16 ++++++++++++---- apps/geocoder/src/worker.py | 9 +++++++-- 5 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 apps/api/migrations/009_band_locations_bigint.sql diff --git a/apps/admin/site/app.js b/apps/admin/site/app.js index 646682f..161ca1b 100644 --- a/apps/admin/site/app.js +++ b/apps/admin/site/app.js @@ -1064,7 +1064,8 @@ async function loadGeocoding() { const locManual = locs.find((x) => x.status === "manual")?.n || 0; const locErr = locs.find((x) => x.status === "error")?.n || 0; const locPct = locTotal ? Math.round((locDone / locTotal) * 100) : 0; - const llmCost = parseFloat(r.llm_cache?.total_cost_usd || 0).toFixed(4); + const rawCost = Number(r.llm_cache?.total_cost_usd); + const llmCost = (Number.isFinite(rawCost) ? rawCost : 0).toFixed(4); const locStatsEl = document.getElementById("loc-stats"); if (locStatsEl) locStatsEl.innerHTML = ` diff --git a/apps/api/migrations/008_band_locations.sql b/apps/api/migrations/008_band_locations.sql index 8de89e4..eb43a0d 100644 --- a/apps/api/migrations/008_band_locations.sql +++ b/apps/api/migrations/008_band_locations.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS band_locations ( id BIGSERIAL PRIMARY KEY, - ma_id INTEGER NOT NULL REFERENCES bands(ma_id) ON DELETE CASCADE, + ma_id BIGINT NOT NULL REFERENCES bands(ma_id) ON DELETE CASCADE, step_order SMALLINT NOT NULL DEFAULT 0, step_label TEXT, location_raw TEXT NOT NULL, diff --git a/apps/api/migrations/009_band_locations_bigint.sql b/apps/api/migrations/009_band_locations_bigint.sql new file mode 100644 index 0000000..2688b2a --- /dev/null +++ b/apps/api/migrations/009_band_locations_bigint.sql @@ -0,0 +1,9 @@ +-- 009_band_locations_bigint.sql +-- bands.ma_id est BIGINT (valeurs jusqu'à ~3.5e9 côté crawler), mais +-- band_locations.ma_id avait été créé en INTEGER (008). Tout INSERT pour un +-- band avec ma_id > 2147483647 échouait en "integer out of range" — l'erreur +-- était avalée silencieusement par l'enqueue, laissant ~62k bands hors pipeline. +-- On aligne le type sur bands.ma_id. + +ALTER TABLE band_locations + ALTER COLUMN ma_id TYPE BIGINT; diff --git a/apps/geocoder/src/enqueue.py b/apps/geocoder/src/enqueue.py index 37ea0c7..d0437f0 100644 --- a/apps/geocoder/src/enqueue.py +++ b/apps/geocoder/src/enqueue.py @@ -43,7 +43,8 @@ def run_enqueue(cur) -> tuple[int, int, int]: bands = cur.fetchall() print(f"[enqueue] {len(bands)} bands à parser") - inserted = country_fixed = skipped = 0 + inserted = country_fixed = skipped = failed = 0 + err_samples: list[str] = [] for ma_id, country, location_text in bands: steps = parse_location_text(location_text, country) @@ -67,8 +68,14 @@ def run_enqueue(cur) -> tuple[int, int, int]: if cur.rowcount > 0: inserted += 1 except Exception as exc: + failed += 1 + if len(err_samples) < 5: + err_samples.append(f"ma_id={ma_id}: {exc}") print(f"[enqueue] WARN insert ma_id={ma_id}: {exc}") + if failed: + print(f"[enqueue] {failed} inserts échoués — échantillon: {err_samples}") + # Résoudre immédiatement les entrées pays-seulement cur.execute( """ @@ -101,7 +108,7 @@ def run_enqueue(cur) -> tuple[int, int, int]: else: print(f"[enqueue] no centroid id={loc_id} raw='{location_raw}'") - return inserted, country_fixed, skipped + return inserted, country_fixed, skipped, failed def main(): @@ -141,10 +148,11 @@ def main(): print(f"[enqueue] job #{job_id} démarré") try: - inserted, country_fixed, skipped = run_enqueue(cur) + inserted, country_fixed, skipped, failed = run_enqueue(cur) summary = ( f"[geocoder_enqueue] job #{job_id} terminé — " - f"inserted={inserted} country_fixed={country_fixed} skipped={skipped}" + f"inserted={inserted} country_fixed={country_fixed} " + f"skipped={skipped} failed={failed}" ) cur.execute( "UPDATE job_triggers SET status='done', finished_at=now() WHERE id=%s", diff --git a/apps/geocoder/src/worker.py b/apps/geocoder/src/worker.py index a71bf4a..202042a 100644 --- a/apps/geocoder/src/worker.py +++ b/apps/geocoder/src/worker.py @@ -66,14 +66,19 @@ def geoapify_search(query: str) -> dict | None: def sync_bands_primary(cur, ma_id: int): - """Met à jour bands.lat/lon avec le step le plus récent géocodé.""" + """Met à jour bands.lat/lon avec le lieu d'ORIGINE géocodé (step_order le plus bas). + + Carte « metal from Europe » : on veut le point de formation du groupe, pas + sa dernière localisation (ex. un groupe de Thessaloniki parti à Boston doit + rester à Thessaloniki, en Europe). + """ cur.execute( """ SELECT lat, lon FROM band_locations WHERE ma_id = %s AND geocode_status IN ('done', 'country_only') AND lat IS NOT NULL AND lon IS NOT NULL - ORDER BY step_order DESC, id DESC + ORDER BY step_order ASC, id ASC LIMIT 1 """, (ma_id,),