docs: capitalize the mairie publication check solution (T2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
346ae78a9a
commit
a8c4012aac
+121
@@ -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]
|
||||
Reference in New Issue
Block a user