Files
echo-du-huit/docs/solutions/2026-07-17-verifier-la-publication-sur-l-agenda-mairie-l-e6h6-solution.md
T

7.1 KiB

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]