// Poseur : lit le storage, écrit le DOM du formulaire mairie, injecte le // bandeau. Volontairement FIN et sans logique métier — tout le calcul est dans // formulaire-valeurs.js, sous tests. Ici, c'est le test humain qui tranche. // // Content script classique : pas d'import, les dépendances arrivent par // globalThis (ordre du tableau `js` du manifest — cf. DECISIONS.md 2026-07-17). // // PAS d'auto-submit : on pré-remplit, l'humain relit et envoie. // RECHERCHE.md §2 (form id = 6). Non touchés délibérément : input_6_34 (image, // impossible par script), input_6_23 / input_6_43 (honeypot), les tokens CSRF, // et input_6_35 (accessibilité : la machine ne sait pas si le lieu est // accessible — cocher au hasard serait un champ rempli faux). const CHAMP_ORGANISATEUR = "input_6_42"; const CHAMP_EMAIL = "input_6_41"; const CHAMP_TITRE = "input_6_1"; const CHAMP_DESCRIPTION = "input_6_3"; const CHAMP_THEME = "input_6_20"; const CHAMP_DATE_DEBUT = "input_6_31"; const CHAMP_DATE_FIN = "input_6_32"; const CHAMP_HEURE_DEBUT = "input_6_38"; const CHAMP_HEURE_FIN = "input_6_39"; const CHAMP_TARIFS = "input_6_40"; const CHAMP_ADRESSE = "nova_address"; const CHAMP_COORDONNEES = "input_6_18"; (async () => { const { EchoConfig, EchoFormulaire } = globalThis; // Storage illisible ou visite directe du formulaire (sans passer par la // liste) : on ne sait pas s'il y a un travail à faire, donc on ne touche à // rien et on n'affiche aucun bandeau. const stock = await lireStock(); if (!stock) return; const payload = stock[EchoFormulaire.CLE_EVENEMENT_EN_ATTENTE]; if (!payload) return; try { // On n'efface PAS le storage à la lecture : le pré-remplissage survit à un // F5 (couture actée — DECISIONS.md 2026-07-14). const config = EchoConfig.fusionner( EchoConfig.DEFAUTS, stock[EchoConfig.CLE_CONFIG_LOCALE], ); const valeurs = EchoFormulaire.calculerValeurs(payload, config); injecterBandeau(valeurs.aCompleter, remplir(valeurs)); } catch (erreur) { // Filet : cette fonction est `async`, une exception y deviendrait une // rejection non gérée — ni remplissage, ni bandeau, ni message. Le bénévole // ne verrait rien du tout. Ce poseur n'étant pas testé unitairement (choix // acté), le filet est sa seule ligne de défense. // Cas réel : la mairie remplace un → `champ.options` // est undefined → TypeError au milieu du remplissage. console.error("Écho du Huit : pré-remplissage interrompu.", erreur); injecterBandeauDegrade(); } })(); async function lireStock() { const { EchoConfig, EchoFormulaire } = globalThis; try { return await browser.storage.local.get([ EchoFormulaire.CLE_EVENEMENT_EN_ATTENTE, EchoConfig.CLE_CONFIG_LOCALE, ]); } catch (erreur) { console.error("Écho du Huit : storage illisible.", erreur); return null; } } // Renvoie les ids qu'on n'a PAS pu remplir (formulaire de la mairie changé ?). function remplir(valeurs) { const manquants = []; // Un champ inattendu (type changé côté mairie) ne doit pas emporter tous les // champs suivants : on le compte comme manquant et on continue. Sans ça, une // seule exception vide silencieusement toute la fin du formulaire. const poser = (poseur, id, valeur) => { try { if (!poseur(id, valeur)) manquants.push(id); } catch (erreur) { console.warn(`Écho du Huit : échec sur #${id}.`, erreur); manquants.push(id); } }; const texte = (id, valeur) => poser(poserTexte, id, valeur); const select = (id, valeur) => poser(poserSelect, id, valeur); // heure === null (journée entière, DTEND absent) : ne rien poser est le // comportement voulu, pas un échec. // Les 4 champs d'heures sont des (vérifié sur le HTML // réel), PAS des selects — contrairement à ce qu'annonçait l'énoncé du // ticket. Ils acceptent « 09 » et « 00 » (chiffres = valid floating-point // number au sens HTML), donc le zéro-padding du calcul convient tel quel. const heure = (prefixe, valeur) => { if (valeur === null) return; texte(`${prefixe}_1`, valeur[0]); texte(`${prefixe}_2`, valeur[1]); }; texte(CHAMP_ORGANISATEUR, valeurs.organisateur); texte(CHAMP_EMAIL, valeurs.email); texte(CHAMP_TITRE, valeurs.titre); texte(CHAMP_DESCRIPTION, valeurs.description); select(CHAMP_THEME, valeurs.theme); texte(CHAMP_DATE_DEBUT, valeurs.dateDebut); texte(CHAMP_DATE_FIN, valeurs.dateFin); heure(CHAMP_HEURE_DEBUT, valeurs.heureDebut); heure(CHAMP_HEURE_FIN, valeurs.heureFin); texte(CHAMP_TARIFS, valeurs.tarifs); texte(CHAMP_ADRESSE, valeurs.adresse); texte(CHAMP_COORDONNEES, valeurs.coordonnees); return manquants; } function poserTexte(id, valeur) { // Une valeur vide se laisse telle quelle : un champ intact est honnête, et // le manque est déjà annoncé dans le bandeau. if (!valeur) return true; const champ = document.getElementById(id); if (!champ) return absent(id); champ.value = valeur; notifier(champ); return true; } function poserSelect(id, valeur) { if (!valeur) return true; const champ = document.getElementById(id); if (!champ) return absent(id); // Match exact : les valeurs du thème sont reprises telles quelles de // RECHERCHE.md §3. Poser une valeur sans option correspondante VIDERAIT le // champ en silence. const option = [...champ.options].find((option) => option.value === valeur); if (!option) { console.warn(`Écho du Huit : aucune option « ${valeur} » dans #${id}.`); return false; } champ.value = option.value; notifier(champ); return true; } // Gravity Forms écoute `change` : poser `.value` seul peut rester invisible du // formulaire. Le dispatch est gratuit et sans risque, on ne le suppose pas. function notifier(champ) { champ.dispatchEvent(new Event("input", { bubbles: true })); champ.dispatchEvent(new Event("change", { bubbles: true })); } function absent(id) { console.warn(`Écho du Huit : champ #${id} introuvable.`); return false; } // Un geste sans retour visible est indiagnosticable : le bandeau dit ce qui a // été fait ET ce qui reste à faire. function injecterBandeau(aCompleter, manquants) { const bandeau = creerBandeau(); bandeau.append( paragraphe("echo-bandeau-titre", "Écho du Huit a prérempli ce formulaire."), paragraphe("echo-bandeau-intro", "À compléter :"), listeDe(aCompleter), paragraphe("echo-bandeau-consigne", "Relis puis envoie toi-même."), ); if (manquants.length > 0) { bandeau.append( paragraphe( "echo-bandeau-alerte", `⚠️ Certains champs n'ont pas pu être remplis (le formulaire de la mairie a peut-être changé) : ${manquants.join(", ")}.`, ), ); } injecter(bandeau); } // Le pré-remplissage a échoué en cours de route : le dire, plutôt que de // laisser le bénévole devant un formulaire à moitié rempli sans explication. function injecterBandeauDegrade() { const bandeau = creerBandeau(); bandeau.append( paragraphe("echo-bandeau-titre", "Écho du Huit n'a pas pu préremplir ce formulaire."), paragraphe( "echo-bandeau-alerte", "⚠️ Le formulaire de la mairie a peut-être changé. Vérifie chaque champ et remplis-le à la main ; le détail de l'erreur est dans la console (F12).", ), ); injecter(bandeau); } function creerBandeau() { const bandeau = document.createElement("aside"); bandeau.className = "echo-bandeau"; return bandeau; } function injecter(bandeau) { const ancre = document.getElementById("gform_wrapper_6") ?? document.getElementById("gform_6"); if (!ancre) { console.warn("Écho du Huit : formulaire introuvable, bandeau non injecté."); return; } ancre.before(bandeau); } // textContent uniquement : jamais d'innerHTML sur une page tierce. function paragraphe(classe, texte) { const p = document.createElement("p"); p.className = classe; p.textContent = texte; return p; } function listeDe(elements) { const liste = document.createElement("ul"); liste.className = "echo-bandeau-liste"; for (const element of elements) { const item = document.createElement("li"); item.textContent = element; liste.append(item); } return liste; }