docs: capitalize the mairie publication check solution (T2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Pierre Martin
2026-07-17 20:06:19 +02:00
co-authored by Claude Opus 4.8
parent 346ae78a9a
commit a8c4012aac
@@ -0,0 +1,121 @@
# Solution : [T2] Vérifier la publication sur l'agenda mairie (+ lien direct)
## Problème résolu
Un bénévole qui avait poussé un événement vers le formulaire mairie n'avait
**aucun retour** : le formulaire est modéré, la publication arrive plus tard, et
rien dans l'Écho du Huit ne disait « c'est en ligne ». On ne savait pas si un
événement était *à faire* ou *déjà publié*, et pour le retrouver sur le site il
fallait le chercher à la main — au risque de le re-soumettre.
Objectif tenu : l'extension **détecte** qu'un événement Nextcloud est réellement
publié sur l'agenda public de la mairie, affiche un badge **« Publié »** + le
**lien direct** vers la fiche, et **retire le bouton « Créer »** (anti-doublon).
Ce statut *publié* est **dérivé** de l'agenda à la volée (décision 2026-06-29),
jamais un tag écrit dans Nextcloud : c'est la mairie qui fait foi, pas notre
intention. Aucun stockage, aucun `credentials` (agenda public).
## Approche choisie
**Scraping HTML de l'agenda + match strict `date locale Cugnaux + titre
normalisé`, biais précision v1** (approche 1 du brainstorm, variante
conservatrice).
Il n'y a **pas d'API mairie** : la seule source est le HTML public. Le vrai
risque n'était donc pas « aller chercher la page » mais **la fiabilité du
match** — et il est gouverné par une **asymétrie de risque** structurante :
- *Faux positif* (marquer « publié » à tort) → le bénévole croit que c'est fait,
l'événement **ne sera jamais publié**. **Grave.**
- *Faux négatif* (rater un match réel) → au pire il revérifie, et le formulaire
**modéré** rattrape un doublon. **Bénin.**
→ On privilégie la **précision** : match strict (date identique + titre normalisé
identique), et dans le doute on **n'affirme pas** « publié ».
Alternatives écartées :
- **Match par slug dérivé** : la slugification WordPress de la mairie n'est pas
connue et ajoute des suffixes (`atelier-dessin-2`) → égalité stricte cassée,
faux négatifs. On matche sur `date + titre` ; le lien est **lu** dans la carte,
jamais reconstruit.
- **Score de confiance + confirmation humaine** des cas ambigus (approche 3) :
meilleur rappel mais 3ᵉ état d'affichage et un état à mémoriser — or *publié*
n'a pas de tag et l'état local est proscrit. Reporté hors v1 ; la porte reste
ouverte si les faux négatifs se voient à l'usage.
## Décisions clés
- **Seam extraction impure / matching pur.** `DOMParser` est **absent de Bun** :
impossible de tester l'extraction DOM en `bun test`. On isole donc
`extraireCartes(html)` (impur, DOMParser, non testé unitairement — validé par
le test humain) du **matching pur** `publication.js`, testé à fond car c'est le
vrai risque produit. Même logique que `formulaire-valeurs.js` (pur, testé) vs
`formulaire-mairie.js` (impur, DOM). La boucle de pagination reste testable en
**injectant** `fetchImpl`/`extraireCartesImpl`.
- **L'agenda est paginé** (`.../agenda/page/N/?f=1&…`). Un seul fetch « période »
ne suffit pas : on boucle jusqu'à une page vide, avec un **plafond
anti-boucle** (12) au cas où un changement HTML masquerait la page vide.
- **Fuseau : jamais de conversion.** Le `<time datetime="YYYY-MM-DD">` est une
date **nue**. On la compare à la date **locale de Cugnaux** du `debut` :
`Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Paris" })` pour l'horodaté,
**composantes locales** (date flottante, sans fuseau) pour la journée entière —
sinon la date glisse d'un jour (piège déjà documenté).
- **Enrichissement additif.** La liste est rendue **immédiatement** ; le badge est
posé *après*, en tâche de fond (`annoterPublications(...).catch(...)`,
fire-and-forget). Un agenda injoignable, un HTTP ≠ 200, un `DOMParser` qui lève
ou une structure changée → `{ ok:false }` → liste **intacte, sans badge**.
Dégradation gracieuse, jamais d'exception, **aucun faux positif**.
- **Périmètre v1 = badge « Publié » + lien seulement.** On ne pose pas encore la
colonne de statut soumis/ignoré (tâche sœur) ; l'ossature reste extensible.
- **Aucune modif de `manifest.json`** : `host_permissions` couvre déjà
`ville-cugnaux.fr`, et les nouveaux modules sont des ESM importés par la page
d'extension (`DOMParser` y est disponible), pas des content scripts.
## Patterns à réutiliser
- **Seam « impur en périphérie, pur au centre ».** Dès qu'une capacité est
indisponible sous Bun (DOMParser, DOM, Gravity Forms), extraire une fonction
fine impure non testée et pousser toute la logique décidante dans un module pur
testé. Injecter les impls (`fetchImpl`, `extraireCartesImpl`) pour tester
l'orchestration sans réseau ni DOM.
- **Normalisation de titre robuste, en une regex.**
`NFD` + suppression des diacritiques, puis `replace(/[^\p{Letter}\p{Number}\s]/gu, "")`
retire **ponctuation ET emojis d'un coup** (ni lettre ni chiffre), casse basse,
espaces compressés. La ponctuation est retirée **sans espace** pour que
« Atelier B.D. » == « Atelier BD ».
- **Enrichissement DOM additif via `dataset.uid`.** Poser `item.dataset.uid` au
rendu, puis retrouver le `<li>` en phase 2 par
`liste.querySelector([data-uid="${CSS.escape(uid)}"])`. La donnée lente ne
bloque jamais l'affichage.
- **Lien externe défensif.** Résoudre le `href` scrapé via `new URL(href, BASE)`
(gère relatif **et** absolu), **rejeter tout schéma non-http(s)**, injecter en
`textContent` + `target="_blank"` `rel="noopener"`. Href absurde → carte
ignorée.
- **`aujourdhui` / `fetchImpl` injectés obligatoires** pour des tests
déterministes (pas de `new Date()` ni de `fetch` caché dans la logique).
## Pièges à éviter
- **Ne pas présumer la balise du scraping.** L'agenda sert des
`<article class="… card-event">`, **pas** des `<div>`. Un sélecteur
`div.card-event` extrait **0 carte** alors que l'agenda répond **200** : panne
**totale et silencieuse** (pas rattrapée par la dégradation gracieuse, qui ne
se déclenche que sur erreur réseau/HTTP). Trouvée par le test humain sur
l'agenda réel (45 cartes attendues, 0 obtenue). **Leçon : sélecteur agnostique
de balise (`.card-event`), et une hypothèse de structure HTML n'est vérifiée
que confrontée au HTML réel.** C'est exactement la zone que le plan réservait
au test humain — le process a joué son rôle.
- **Ne pas reconstruire le lien depuis le slug** : suffixes WordPress imprévisibles
(`-2`). Le lien se **lit** dans la carte.
- **Ne pas convertir le fuseau** d'une date nue ou d'une journée entière → dérive
d'un jour. Invisible sous `TZ=Europe/Paris`, d'où la variable portée par le
script de test.
- **Ne pas laisser flotter la promesse d'annotation** : `.catch` explicite, sinon
une erreur d'indexation devient une unhandled rejection.
- **Le `console.error "agenda mairie injoignable"` en sortie de test est
attendu** (test de dégradation gracieuse), pas un échec.
## Tags
tags: [firefox-extension, mv3, javascript, web-scraping, dom-parsing, bun-test,
pure-impure-seam, timezone, string-normalization, graceful-degradation,
nextcloud, wordpress]