feat: prefill the mairie form from the picked event
Wire the seam end to end: the background mirrors config.local.json into storage (a content script would need web_accessible_resources, which would expose the email to the mairie page), and the content script lays the computed values into the form. The layer is deliberately thin and untested: all the logic lives in formulaire-valeurs.js. It never writes an empty value, never touches the image, the honeypots or the CSRF tokens, and never submits: a human reads and sends. A banner states what is left to do, and warns when a field could not be filled, so that a silent failure stays diagnosable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
78153557dd
commit
2e60bd08d8
@@ -0,0 +1,237 @@
|
||||
// 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 <select> par un <input> → `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 <input type="number"> (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;
|
||||
}
|
||||
Reference in New Issue
Block a user