-- 014_run_cancellation_and_geo_sync.sql -- -- Deux corrections d'invariants qui rendaient des fonctionnalités cosmétiques. -- -- A) Annulation réelle des crawl_run -- Avant : « Annuler » écrivait status='error' en base. Le crawler Python ne -- lisait jamais cette colonne et continuait sa boucle jusqu'au bout. Pire, -- son UPDATE final `WHERE id=%s AND status='running'` ne matchait plus, donc -- un run mené à terme restait affiché en erreur. Le bouton ne faisait rien -- d'autre que mentir. -- Après : l'admin pose un DRAPEAU (cancel_requested) sans toucher au statut. -- Le crawler le lit à chaque itération et s'arrête proprement, en écrivant -- lui-même status='cancelled'. Le statut reflète donc toujours la réalité du -- process. -- -- B) Désynchronisation du point principal -- bands.lat/lon/geom est une dénormalisation du PREMIER lieu géocodé de -- band_locations (voir _sync_band_point dans geocoder/worker.py). Le trigger -- de la migration 013 supprime les band_locations quand location_text change, -- mais laissait l'ancien point sur bands : la carte (qui lit band_locations) -- n'affichait plus rien tandis que la liste et la heatmap (qui lisent bands) -- continuaient de montrer l'ancienne ville, parfois pendant des jours. -- ------------------------------------------------------------------ -- A) Annulation coopérative -- ------------------------------------------------------------------ ALTER TABLE crawl_run ADD COLUMN IF NOT EXISTS cancel_requested BOOLEAN NOT NULL DEFAULT FALSE; ALTER TABLE crawl_run ADD COLUMN IF NOT EXISTS cancel_requested_at TIMESTAMPTZ; ALTER TABLE crawl_run ADD COLUMN IF NOT EXISTS cancel_requested_by TEXT; -- Le crawler interroge ce drapeau en boucle : index partiel pour que le -- lookup reste un index-only scan même avec un historique de runs volumineux. CREATE INDEX IF NOT EXISTS idx_crawl_run_cancel ON crawl_run (id) WHERE status = 'running' AND cancel_requested = TRUE; -- Note : on n'ajoute délibérément PAS de cancel_requested sur job_triggers. -- Un job 'pending' se retire de la file (c'est ce que fait la route existante) ; -- une fois réclamé par le crawler il n'existe plus qu'à travers le crawl_run -- qu'il a créé, et c'est CE run qu'on annule. Deux drapeaux pour un seul état -- réel, c'est exactement le genre de colonne qui finit morte. -- ------------------------------------------------------------------ -- B) Le point principal suit la suppression des localisations -- ------------------------------------------------------------------ CREATE OR REPLACE FUNCTION bands_geocode_dirty() RETURNS trigger AS $$ BEGIN IF TG_OP = 'UPDATE' AND OLD.location_text IS DISTINCT FROM NEW.location_text THEN DELETE FROM band_locations WHERE ma_id = NEW.ma_id; -- Purge aussi le point dénormalisé : il décrivait l'ancienne localisation. -- Cet UPDATE ne touche pas location_text, donc il ne redéclenche pas ce -- trigger (défini AFTER UPDATE OF location_text) — pas de récursion. UPDATE bands SET lat = NULL, lon = NULL, geocoded_at = NULL, geocode_provider = NULL, geocode_query = NULL WHERE ma_id = NEW.ma_id AND (lat IS NOT NULL OR lon IS NOT NULL); -- geom est recalculé (à NULL) par le trigger BEFORE bands_set_geom. END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- ------------------------------------------------------------------ -- C) Localisation posée manuellement par un admin -- ------------------------------------------------------------------ -- Éditer lat/lon depuis le dashboard n'écrivait que sur bands : la carte, qui -- clusterise depuis band_locations, ignorait totalement la correction. On -- réserve step_order = -1 à l'override manuel, qui passe donc avant tous les -- steps issus du parsing (tri step_order ASC) et devient le point principal. COMMENT ON COLUMN band_locations.step_order IS 'Ordre de l''étape dans location_text (0 = première). -1 = override manuel admin, prioritaire.';