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>
78 lines
4 KiB
PL/PgSQL
78 lines
4 KiB
PL/PgSQL
-- 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.';
|