`npm run check` échouait par intermittence sur :
Error: ENOENT: no such file or directory, open
'vitest.config.js.timestamp-1787150578770-50269fd6fd11e.mjs'
Vitest écrit une version transpilée de sa config à côté d'elle au démarrage,
puis la supprime aussitôt. Depuis que les vérifications tournent en parallèle,
ESLint apercevait ce fichier pendant son balayage et il avait disparu au
moment de le lire.
Course non déterministe : elle ne se déclenchait que si les deux tâches se
croisaient au mauvais instant, d'où un gate qui échoue sans raison apparente
un push sur trois — le pire mode de défaillance pour une porte de qualité,
puisqu'il pousse à utiliser --no-verify par réflexe.
Motif ajouté aux ignores. Trois exécutions consécutives vertes.
Chaque push affichait les 27 mêmes avertissements. Un gate qui répète
toujours la même chose n'est plus lu — et le jour où un VRAI avertissement
apparaît, il passe inaperçu au milieu. Les deux règles en cause étaient les
miennes et sont inadaptées à ce code.
require-await (7 occurrences, apps/api)
Inadapté à Fastify : un handler `async` sans `await` est la façon normale de
renvoyer une valeur — l'API distingue le style async du style callback avec
reply.send. Les rendre synchrones changerait la sémantique. Règle désactivée
pour l'API.
no-console (20 occurrences, apps/web/site)
Traces de diagnostic délibérées dans le site public. Les signaler à chaque
exécution sans jamais les retirer ne faisait que noyer le reste. Règle
désactivée pour les frontends, avec la valeur à restaurer si on décide un
jour de les nettoyer.
Le hook affiche désormais six lignes vertes et rien d'autre : tout ce qui
s'imprime redevient un signal.
Le panneau était découpé par table SQL, pas par question que se pose l'admin.
Conséquence : « je constate ici, j'agis dans un autre onglet », et aucun moyen
de voir ce qui coince réellement.
Redécoupage — un onglet = une question
- Pilotage : est-ce que ça tourne ? (fusionne l'ancien « Actions »)
- Groupes : trouver et corriger
- Localisations : qu'est-ce qui coince dans le géocodage ? ← NOUVEAU
- Journal : que s'est-il passé ?
- LLM & coûts : combien ça coûte ?
Pilotage : chaque chiffre problématique porte son action
- Bandeau de santé (groupes, géocodage, file, bloqués, coût) coloré par état
- Alertes actionnables : « 12 localisations en erreur » + [Examiner] +
[Tout remettre en file], au lieu d'un mur de boutons dans un autre onglet
- Déclencheurs de traitements inline, opérations destructives repliées
- L'ancien bloc « Éléments de genre » (des centaines de mots-clés jamais
consultés) est retiré
Localisations : la vue qui manquait totalement
L'admin ne disposait que d'actions EN MASSE sur band_locations et d'aucun moyen
de voir CE qui échouait. Débloquer un seul lieu supposait de relancer des
milliers d'appels Geoapify/Groq facturés.
- GET /admin/api/locations liste filtrable (statut, lieu, groupe, pays)
- POST /admin/api/locations/:id/requeue relance UNE ligne
- PATCH /admin/api/locations/:id saisie manuelle des coordonnées
- Diagnostic lisible sans clic : lieu brut, statut, essais, erreur réelle
Groupes : recherche d'abord
Une barre de recherche et des filtres rapides en chips remplacent les 8 champs
texte ; les filtres avancés sont repliés.
Accessibilité
- Lignes de tableau activables au clavier (role=button, tabindex, Entrée)
- Modales : Échap ferme, focus piégé, focus rendu à l'élément d'origine
- Contraste : --err (#c61a1a) échouait WCAG AA en texte sur fond sombre (3,4:1).
Les messages d'erreur étaient difficiles à lire. Ajout de --err-text /
--ok-text (~6,5:1) pour les usages en couleur de texte.
Détecté par les nouveaux tests axe sur les vues rendues.
- Statuts affichés en français au lieu des valeurs brutes de la base
Tests
- 36 tests API sur les trois nouveaux endpoints (allowlist de statuts,
bornes des coordonnées, audit, 404)
- 53 parcours Playwright sur la VRAIE app Fastify + faux pool, dans une
topologie identique à la production (statique servi + /admin/* proxifié)
- Tests axe sur les vues RENDUES : le test jsdom existant ne voyait que la
coquille vide du dashboard, tout étant construit en JavaScript
- e2e branché sur le hook pre-push, pas sur `check` : la boucle de
développement reste à 6 s, le push coûte 28 s
Correction trouvée par les tests
Le routeur ne séparait pas la query string du nom de vue :
« #/locations?status=error » ne correspondait à aucun alias et retombait sur
Pilotage — les liens des alertes ne fonctionnaient pas.
Co-Authored-By: Claude <noreply@anthropic.com>
Passe de performance guidée par la mesure, pas par l'intuition.
Porte de qualité (`npm run check`) — la commande qui tourne des dizaines de
fois par jour :
- Les 5 vérifications sont indépendantes : exécutées en parallèle via
scripts/check.mjs au lieu d'un enchaînement `&&` qui imposait la somme des
durées ET 7 démarrages npm imbriqués. 14 s → 6 s
- Cache ESLint (--cache) et compilation incrémentale tsc. 16 s → 14 s
- Chemin critique restant : vitest 4,9 s, dont ~2 s de mise en place jsdom.
Tests de mutation :
- Config Vitest dédiée en pool `threads` sans ré-isolation. Mesuré sur le
dry-run Stryker : overhead d'amorçage 12 844 ms → 1 592 ms, soit ~90 % du
cycle par mutant. 5 min 02 → 3 min 40
- Le mode incrémental ramène le profil complet à 12 s après une édition.
Pistes mesurées puis REJETÉES (documentées dans README-CI.md pour éviter
qu'on les retente) :
- Découper la mutation en 5 processus Stryker parallèles : 262 s contre 235 s.
Chaque processus repaie son bac à sable et son dry-run.
- Monter `concurrency` de 6 à 16 : aucun effet (~8,5 mutants/s partout), le
débit est borné par l'orchestration mono-processus de Stryker.
- Séparer vitest API/frontend en deux processus : aucun gain, jsdom domine.
- Pool `threads` sur la suite complète : 18,8 s contre 3,5 s. Le bon réglage
dépend du jeu de fichiers (jsdom ou non) — d'où deux configs distinctes.
Le périmètre muté est inchangé (1511 mutants) : un score obtenu en retirant
les mutants gênants vaudrait moins que pas de score.
Corrections au passage :
- scripts/*.mjs n'était couvert par aucune section ESLint
- Les deux suites pytest ont chacune un paquet `src` : réunies dans un seul
processus, le premier importé masquait l'autre. Séparées (et parallèles).
Co-Authored-By: Claude <noreply@anthropic.com>
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>