Diwall

Français
Télécharger 1.24.4

Notes de terrain

Chaque friction rencontrée en utilisant Diwall pour un vrai travail, numérotée et datée depuis mai 2026 — y compris celles qui venaient de nos propres erreurs.

Sommaire

Depuis les premières sessions, chaque friction rencontrée en faisant un vrai travail avec Diwall a été notée : ce qui a été tenté, ce qui s’est passé, pourquoi, et ce qu’on en a fait. Plus d’une centaine d’entrées, numérotées et datées.

Elles sont la matière première des recettes — une recette est une friction résolue assez souvent pour mériter d’être généralisée.

Ce qu’on y met

Pas seulement les frictions dues à Diwall. Le carnet contient aussi :

Nos propres erreurs. Le mot de passe d’une application de développement locale, resté des semaines dans l’historique public parce qu’il ne protégeait rien et semblait inoffensif à l’époque. Une page de test synthétique substituée à celle que la spécification demandait, et présentée comme conforme. Les deux consignées avec le même détail que n’importe quel défaut de l’outil.

Des frictions qui n’étaient pas celles de Diwall. Un problème de synchronisation imputé à l’outil pendant deux sessions avant que la vraie cause n’apparaisse ailleurs. Le mauvais diagnostic reste dans le carnet à côté du bon, parce que c’est le mauvais diagnostic qui a coûté le temps.

Ce qui a marché, et pourquoi. Un répertoire chiffré si transparent que l’opérateur ne l’a jamais remarqué. Un arbre d’accessibilité qui a suffi à tout un parcours de site sans appeler le moindre modèle de vision.

La règle qui le rend utilisable

Une entrée s’écrit quand la friction survient, pas quand elle est résolue.

Écrite après coup, une friction devient une histoire bien rangée avec une cause nette. Écrite sur le moment, elle garde les fausses pistes — et ce sont les fausses pistes qu’un lecteur reconnaît quand il est bloqué au même endroit.

C’est aussi pourquoi certaines entrées se terminent sans solution. Une friction encore ouverte est plus honnête en entrée ouverte qu’en silence.

Ces notes sont en français

C’est le carnet de terrain d’un opérateur français, écrit dans la langue où le travail s’est fait. Le traduire lisserait le registre — une note de terrain est idiomatique, parfois abrupte, et sa traduction automatique se lit comme un rapport.

Il reste dans sa langue d’origine, et ses traductions, quand elles existent, sont signalées comme des transcriptions, pas présentées comme des équivalents. Le jour où un opérateur germanophone écrira le sien, la même règle s’appliquera en sens inverse.

À quoi il sert

Pour nous : ne pas résoudre deux fois le même problème, et savoir quelles limites sont réelles.

Pour vous : savoir ce que cet outil coûte vraiment, y compris les jours où rien ne marche, avant de vous y engager.

Recettes → · Limites →

Le carnet lui-même suit, en français, exactement tel qu’il a été écrit.

1. Le piège de Mode B (sessions ReAct)

Au départ, j’ai cru qu’il fallait --sauver-session / --reprendre-session pour tout parcours multi-étapes. Réflexe naturel : « ReAct = un pas à la fois ».

Réalité : Mode B coûte cher — chaque appel relance Playwright, recharge le storage_state, redessine le SoM. Pour un formulaire de 3 champs + submit, c’est 4 captures × ~2 s = 8 s minimum, plus le temps LLM entre chaque pas.

Ce qui marche : --action accepte un tableau d’actions atomiques. Une seule invocation enchaîne fill+fill+fill+click, une seule capture finale.

--action '[
  {"type":"remplir_som","id":1,"valeur":"foo"},
  {"type":"remplir_som","id":2,"valeur":"bar"},
  {"type":"cliquer_som","id":3}
]'

Heuristique : Mode B uniquement quand le DOM change entre deux pas et que je dois voir ce changement pour décider du suivant.

2. --output sans extension → mime type None

J’ai passé --output /tmp/test (sans .png). Erreur peu parlante : unknown mime type None. Quelques minutes perdues à chercher si c’était un problème Playwright, un PATH, une perm.

Origine : Playwright déduit le format d’image de l’extension du chemin de sortie. Pas d’extension, pas de mime type.

Fix côté utilisateur : toujours .png (ou laisser Diwall générer le nom automatique dans /tmp/diwall/).

Fix côté Diwall (suggestion) : ajouter .png si manquant, ou échouer tôt avec un message explicite.

3. remplir plante quand le sélecteur matche plusieurs éléments

Tentative naïve : {"type":"remplir","selecteur":"input","valeur":"foo"}. Le formulaire a 3 inputs → Playwright refuse (strict mode violation).

Lecon : remplir (CSS) est piégeux dès qu’il y a plusieurs candidats. Le SoM résout ça nativement — chaque élément a un ID unique.

Règle perso : préférer remplir_som par défaut, garder remplir pour les cas où l’on sait que le sélecteur est unique (ex : #username).

4. remplir_som sur input pré-rempli : le triple-clic rate

Le formulaire avait un champ URL pré-rempli avec https://. J’ai voulu écrire https://ada.example.com. Diwall a tapé ada.example.com après le contenu existant → résultat : https://ada.example.com (par chance correct au premier test), mais sur le troisième champ URL, le triple-clic de sélection n’a pas pris → le préfixe est resté → domaine créé = ada au lieu de ada.example.com.

Origine : remplir_som fait triple-click → fill. Sur certains inputs (autocomplete, masques), le triple-click ne sélectionne pas tout le contenu.

Workaround : précéder le fill d’un clear explicite, ou utiliser element.fill('') puis element.fill(value) côté Playwright.

Fix côté Diwall (suggestion) : faire element.fill('') avant le fill réel, plutôt que de compter sur le triple-click.

5. Session PHP non persistée entre appels Mode B

Login réussi → capture montre la page authentifiée. Action suivante en --reprendre-session → redirection vers /login. Cookies perdus.

Origine : storage_state capture cookies + localStorage, mais pas la session côté serveur. Si le serveur PHP refait session_start() avec un session ID périmé (ex : régénéré post-login), le cookie côté client n’aide pas.

Dans le cas Sillage, c’était plus subtil : session_regenerate_id() après login changeait l’ID, et --sauver-session capturait l’ancien. La fenêtre de validité du nouveau cookie n’était pas dans le snapshot.

Workaround : enchaîner login + action suivante en un seul Mode A multi-actions. Ou refaire le login à chaque étape (coûteux).

Mention dans GUIDE_LLM.md : la note SPA §3 dit « Form values are NOT persisted in storage_state » — c’est vrai, mais le problème session-côté-serveur mériterait sa propre ligne.

6. button:has-text() — la syntaxe Playwright qui n’est pas du CSS

Réflexe : {"selecteur":"button:has-text('Connexion')"}. C’est de la syntaxe Playwright (pseudo-sélecteur étendu), pas du CSS standard. Diwall passe ça tel quel à Playwright → ça marche la plupart du temps, mais ce n’est pas documenté dans GUIDE_LLM.md.

Suggestion doc : ajouter au tableau des actions une ligne « Playwright extended selectors supportés (:has-text(), :visible, etc.) » ou au contraire les interdire pour rester strictement CSS.

Synthèse session 1 — ce qui m’aurait fait gagner du temps

  1. Mode A multi-actions par défaut, Mode B comme exception → à dire plus fort dans GUIDE_LLM.md. La doc actuelle présente Mode B en premier (§78), ce qui suggère que c’est le mode normal.
  2. Coup d’œil dès le départ sur les pièges du SoM — pré-remplis, autocomplete, inputs maskés. Une section « SoM edge cases » manque.
  3. Un exemple de scénario complexe (login + form + submit) dans examples/ aiderait à voir le pattern « tout en une action ».

Le reste — l’idée d’avoir des yeux, la boucle Read(PNG) → analyse → fix, le coût marginal d’une capture, la rapidité du SoM vs cliquer_visuel — c’est aussi bon que la doc le promet. Une fois ces six pièges connus, on travaille vite.


Session 2 — Pierre v4 sur Sillage (19 mai 2026, après-midi)

Validation E2E du clonage VPS → HOST_ADMIN via l’UI Sillage (post-R13). Six nouvelles frictions sont apparues, distinctes de celles de la session 1.

7. attendre vs pause — deux verbes pour deux choses

Réflexe : {"type":"attendre","duree":1500} pour insérer un délai de 1,5 s entre deux clics. Diwall lève KeyError: 'selecteur'. Confusion : « attendre » en français suggère un sleep ; en réalité c’est page.wait_for_selector().

Origine : attendre mappe sur page.wait_for_selector(a["selecteur"], …). Champ requis : selecteur. La pause temporelle, c’est pause avec ms.

Friction ressentie : l’erreur KeyError 'selecteur' ne dit pas « vous vouliez peut-être pause ? ». Plusieurs minutes à relire shot.py avant de comprendre que attendre ≠ sleep.

Workaround : utiliser {"type":"pause","ms":1500}.

Suggestion doc : renommer ou doc claire — attendre_selecteur (alias explicite) et garder attendre en deprecated. Ou renommer en wait_for / sleep pour lever toute ambiguïté linguistique.

8. <dialog> HTML modal disparaît après --reprendre-session

Scénario : Pierre clique un bouton qui appelle dialog.showModal(). La capture SoM montre bien les éléments de la modal (id=18 « Annuler », id=19 « Lancer le clonage »). Action suivante : --reprendre-session puis cliquer_som id=19 → élément SoM 19 non trouvé sur la page.

Origine : storage_state capture cookies + localStorage + URL, mais pas l’état JS d’un <dialog> ouvert via showModal(). À chaque --reprendre, Playwright fait page.goto(url) → la page recharge → la modal disparaît.

Friction ressentie : croire que la session est un snapshot complet de l’état UI. C’est faux. La session est un snapshot transactionnel : cookies + storage, pas l’état des composants JS.

Workaround : enchaîner cliquer-bouton-qui-ouvre-modal + pause + cliquer-bouton-de-la-modal dans une seule invocation Mode A. Pas de --reprendre-session entre les deux.

Suggestion doc : ajouter dans GUIDE_LLM.md une note sur les pièges de la reprise de session — modals, popovers, focus, sélections, scroll, champs dirty.

9. Sélecteurs Playwright étendus pas tous supportés (:left-of, etc.)

Tentative : button:has-text("↺ Cloner"):left-of(:has-text("Gérer →")). Playwright supporte officiellement :left-of(), mais le verbe cliquer de Diwall fait page.click(selector) qui ne semble pas activer ces extensions par défaut → timeout 120 s sans capture intermédiaire utile.

Origine : la chain de sélecteurs Playwright a deux modes — les pseudos classiques (:has-text, :visible, :nth-match) qui marchent partout, et les pseudos relationnels (:left-of, :right-of, :near) qui nécessitent Playwright >= 1.18 et parfois un engine explicite.

Friction ressentie : la doc Playwright dit « ça marche », mais sans préciser que c’est sensible aux versions. Diagnostic compliqué — cliquer ne fait pas de capture d’échec utilisable (juste le timeout).

Workaround : préférer des sélecteurs intrinsèques à l’élément cible plutôt que relationnels. Ici, le bouton avait title="Cloner __HOST_ADMIN__ (WordPress) depuis le VPS vers __HOST_ADMIN__" → sélecteur unique : button[title*="Cloner __HOST_ADMIN__"][title*="(WordPress)"].

Leçon : SoM > sélecteurs CSS relationnels > sélecteurs CSS basiques. Quand un élément a un attribut HTML unique (title, aria-label, id), l’utiliser plutôt que sa position.

10. Plusieurs boutons identiques — :has-text() matche le premier

La vue projet contenait trois boutons « ↺ Cloner WordPress » (un par domaine). Le sélecteur button:has-text("↺ Cloner WordPress") matche le premier rencontré dans le DOM. Pierre ne sait pas lequel il va lancer.

Origine : page.click(selector) sans qualificatif. Playwright en mode strict refuserait, mais Diwall ne semble pas le forcer.

Friction ressentie : « j’ai cliqué Cloner pour un domaine donné, et le log montre qu’un autre domaine a démarré ». Inversion silencieuse.

Workaround : sélecteur précis via title= (qui contient le nom du domaine), ou via SoM (chaque bouton a un id SoM unique).

Suggestion Diwall : activer le strict mode Playwright par défaut, ou au moins le surfacer dans GUIDE_LLM.md. Un échec strict-mode est BIEN plus utile qu’un clic silencieux sur le mauvais élément.

11. Pas de visibilité sur une action longue (clonage 2-3 min)

Le clonage WordPress prend ~2 min. Diwall capture après la pause finale. Entre temps, l’interface affiche peut-être un spinner, une barre de progrès, des logs en temps réel — Pierre ne voit rien. Pour suivre l’exécution j’ai dû ouvrir une session SSH en parallèle (tail -f du log côté serveur).

Origine : Diwall = capture terminale. Le modèle « 1 invocation = 1 PNG » n’autorise pas le streaming.

Friction ressentie : 3 minutes d’attente aveugle. Si le clonage avait planté à 30 s, je l’aurais su qu’au bout de 3 min. Et pour itérer, c’est 2-3 min de plus à chaque test.

Workaround : insérer plusieurs {"type":"capturer","nom":"intermediaire-N"} dans la séquence d’actions. Chaque appel capturer génère un PNG dans output-dir, on peut faire Read dessus à la fin et inspecter visuellement les étapes.

Suggestion Diwall : option --stream-toutes-les-Ns 10 qui capture automatiquement toutes les 10 s pendant les pause longues, et stocke les PNG dans output-dir/. Très utile pour le debug.

12. ~/Vaults/Diwall par défaut, pas ~/Vaults/<Projet>/Diwall

Le vault par défaut est ~/Vaults/Diwall/<domaine>.json. L’opérateur peut avoir historiquement rangé les credentials en ~/Vaults/<PROJET>/Diwall/.... La première tentative remplir … "valeur":"depuis_vault" a échoué parce que Diwall cherchait ~/Vaults/Diwall/__HOST_ADMIN__.json (absent).

Origine : lib/vault.py lit DIWALL_VAULT_DIR (env var), sinon défaut ~/Vaults/Diwall. La doc ne dit pas que la convention multi-projet ~/Vaults/<Projet>/Diwall/ est gérée à la main par l’utilisateur via env.

Friction ressentie : « j’ai mis les credentials au bon endroit, pourquoi ça ne marche pas ? ». Cinq minutes de find pour réaliser que c’était une question de variable d’env.

Workaround : DIWALL_VAULT_DIR=~/Vaults/<PROJET>/Diwall devant l’invocation, à chaque fois.

Suggestion Diwall : (a) lire un fichier de config par projet (.diwall.toml à la racine du dépôt courant qui pointe le vault dir), ou (b) une convention « auto-détecter ~/Vaults/<basename-du-cwd>/Diwall/ ».

Synthèse session 2 — ce qui se confirme

Mode A multi-actions reste la voie royale — confirmé pour la 2e fois. Pierre v4 a tenté Mode B pour la modal post-clic, ça a foiré (friction #8).

  • SoM mieux que CSS relationnel — friction #9. SoM > :left-of > tout.
  • Sélecteurs intrinsèques à l’élément (title=, aria-label, id)

    sélecteurs textuels — friction #10.

  • Captures intermédiaires sont sous-utilisées — friction #11. À populariser dans la doc.

Bilan : 12 frictions sur 2 sessions. La 1ère a découvert les pièges structurels (Mode B, session, :has-text, SoM edge cases) ; la 2e les pièges contextuels (sémantique des verbes, état modal, sélecteurs multi-match, suivi d’action longue, vault). Diwall reste un outil d’une puissance rare une fois ces pièges connus.


Session 3 — Validation R33-bis sur Sillage (21 mai 2026, après-midi)

Session courte : valider le rendu de trois pages de l’interface Sillage après un changement back-end (durcissement de config.sh). Deux frictions, toutes deux liées à l’enchaînement d’un parcours authentifié.

13. naviguer — valider plusieurs pages authentifiées en une invocation

Les sessions 1 et 2 ont établi « Mode A multi-actions = voie royale » pour un formulaire sur une page. Restait une question : comment valider le rendu de plusieurs pages derrière un login, sans --reprendre-session (proscrit, frictions #5 et #8) ?

Réflexe initial : une invocation par page, en rejouant le login à chaque fois. Coûteux et inutile.

Réalité : le verbe naviguer ({"type":"naviguer","url":"…"}) fait un page.goto() en conservant le contexte navigateur — donc la session PHP obtenue au login. Une seule invocation Mode A enchaîne login + naviguer vers la page A + capturer + naviguer vers la page B + capturer, etc.

Pattern : pour valider N pages authentifiées, une liste d’actions unique — remplir_som/cliquer_som (login), puis pour chaque page naviguer + attendre_navigation + capturer. Chaque capturer produit un PNG dans output-dir. Trois pages validées en une invocation, ~4 s.

Leçon : naviguer étend « Mode A = voie royale » du multi-action au multi-page. --reprendre-session n’est jamais nécessaire pour un simple parcours de validation.

14. Les fichiers scenarios/*.json ne sont pas consommables tels quels

/opt/diwall/scenarios/sillage_login.json est un objet {"nom":…, "url":…, "actions":[…]}. Réflexe : --actions scenarios/sillage_login.json.

Réalité : --actions (comme --action) attend un tableau d’actions. charger_actions fait json.load et renvoie l’objet ; executer_actions itère alors les clés du dict ("nom", "url", "actions") et plante sur "str".get("type"). Aucun flag --scenario n’existe.

Friction ressentie : le répertoire scenarios/ ressemble à une bibliothèque prête à l’emploi ; en fait ses fichiers sont des gabarits. Pour s’en servir, il faut extraire .actions (à passer à --actions) et .url (à passer à --url) séparément.

Suggestion Diwall : soit un flag --scenario qui lit l’objet complet (url + actions), soit faire détecter à charger_actions un objet contenant une clé actions et en extraire le tableau.

Synthèse session 3

naviguer complète le tableau : Mode A couvre désormais explicitement le parcours multi-page authentifié — friction #13.

  • Le format des scenarios/*.json (objet) diverge de ce que --actions consomme (tableau) — friction #14.

14 frictions sur 3 sessions. La 3e était une session de validation pure : deux frictions seulement, signe que l’outil est maîtrisé une fois les sessions 1 et 2 digérées.


Session 4 — Validation suppression en lot Sillage (29 mai 2026)

Session de validation UI : tester la suppression en lot de clones WordPress depuis la vue domaine de l’interface Sillage (cases à cocher + barre d’actions). Deux frictions, l’une bloquante (manque d’action dans shot.py), l’autre préventive (workaround déjà connu mais non documenté pour rpa.py).

15. remplir incompatible avec <select> — action select_option manquante

RÉSOLU le 29/05/2026 (commit 4bc463e). remplir_som détecte désormais tag == "SELECT" et applique el.value = valeur; el.dispatchEvent(new Event('change', {bubbles:true})) via page.evaluate(), au lieu de fill(). Plus besoin d’action select_option dédiée : on cible le <select> par son ID SoM avec remplir_som. Validé E2E (suppression en lot Sillage, session 5).

Besoin : choisir une option dans un <select> HTML (la barre d’actions en lot de Sillage utilise un <select id="select-action-lot">).

Tentative : {"type":"remplir","selecteur":"#select-action-lot","valeur":"supprimer"}.

Erreur :

Locator.fill: Error: Element is not an <input>, <textarea> or [contenteditable] element

fill() Playwright ne supporte que les inputs textuels. Pour un <select>, Playwright expose select_option().

shot.py n’a pas d’action select_option → blocage total sur tout scénario nécessitant un <select>.

Workaround côté LLM : aucun workaround propre. On peut tenter un evaluate JavaScript, mais shot.py ne supporte pas evaluate. Le scénario s’arrête là.

Fix côté Diwall (suggestion d’implémentation) :

elif t == "select_option":
    page.locator(a["selecteur"]).select_option(a.get("valeur", ""), timeout=timeout)

À ajouter dans le bloc elif de executer_actions(), entre remplir et cliquer. Usage dans un scénario :

{"type": "select_option", "selecteur": "#select-action-lot", "valeur": "supprimer"}

Impact : toute interface utilisant des <select> est actuellement non testable via scénario rpa.py ou Mode A. Fréquent dans les UI CRUD (filtres, actions en lot, sélection de rôle…).

16. Navigation post-login RPA retombe sur la page de login sans pause

Dans un scénario rpa.py enchaînant login + naviguer, la navigation vers la page cible retombait sur la page login si aucune pause n’était insérée après le clic « Se connecter ».

Scénario défaillant :

{"type":"cliquer_som","id":2},
{"type":"attendre_navigation"},
{"type":"naviguer","url":"https://…/page-cible"}

Scénario fonctionnel :

{"type":"cliquer_som","id":2},
{"type":"pause","ms":2000},
{"type":"naviguer","url":"https://…/page-cible"}

Origine : attendre_navigation ne suffit pas — il détecte networkidle mais la session PHP côté serveur n’est pas encore validée (session_regenerate_id post-login, cf. friction #5). La pause 2000ms laisse le serveur émettre le cookie de session régénéré avant la prochaine navigation.

Lien avec friction #5 : même cause racine. Ici c’est dans rpa.py au lieu de Mode B, mais le diagnostic est identique : attendre_navigation ≠ « session serveur prête ».

Suggestion doc : ajouter dans GUIDE_LLM.md (section RPA ou parcours authentifiés) : “Après un submit de formulaire de login, insérer systématiquement {"type":"pause","ms":2000} avant toute naviguer vers une page protégée. attendre_navigation ne garantit pas que la session serveur est propagée.”

Demande architecturale : documentation LLM avec mémoire des régressions

Au-delà des frictions techniques, une demande structurelle a émergé lors de cette session : les LLM qui utilisent Diwall redécouvrent les mêmes frictions à chaque session, car le RETOUR_EXPERIENCE.md est riche mais non synthétisé en règles actionnables.

Besoin :

  1. Une documentation LLM courte (< 2 pages) listant les règles définitives — ce qui fonctionne, ce qui est proscrit, les workarounds validés. Format “règle → contexte → exemple”, pas de narratif.
  2. Un registre de scénarios testés et validés (avec leurs résultats attendus) pour détecter les régressions entre versions de shot.py.

Suggestion : créer docs/GUIDE_LLM_REGLES.md (la synthèse des 16 frictions en 20 règles numérotées) et docs/SCENARIOS_VALIDES.md (table des scénarios avec version shot.py, résultat attendu, date de dernière validation).

Synthèse session 4

select_option manquant dans shot.py → bloquer sur tout <select> HTML (friction #15 — bloquante, demande un correctif shot.py).

  • Pause obligatoire post-login dans rpa.py (friction #16 — workaround connu, à documenter explicitement dans GUIDE_LLM.md).
  • Demande architecturale : doc LLM synthétique + registre de scénarios validés.

16 frictions sur 4 sessions.


Session 5 — Reconnexion HOST_VPS → HOST_ADMIN sur Sillage (29 mai 2026, soir)

Session longue et de bout en bout : reconnecter un client hébergé sur un serveur Plesk à comptes cloisonnés (un utilisateur à droits minimaux par site), via l’UI Sillage pilotée par scénarios rpa.py — ajout serveur, sonde, création des domaines, puis clonage WordPress distant→local. Friction #15 confirmée résolue. Trois nouvelles frictions, dont une structurante.

17. --scenario existe désormais — mais attend un chemin, pas un nom

La suggestion de la friction #14 a été implémentée ✅ : rpa.py a maintenant un flag --scenario qui lit l’objet complet {url, actions}. Mais il attend un chemin de fichier, pas un nom de scénario du répertoire scenarios/.

--scenario sillage_voir_client → {"erreur":"fichier_introuvable"}. Il faut --scenario /opt/diwall/scenarios/sillage_voir_client.json.

Friction ressentie : le répertoire scenarios/ ressemble à une bibliothèque adressable par nom ; en réalité il faut le chemin complet. (Et il n’y a pas de wrapper diwall.sh — l’invocation est venv/bin/python3 rpa.py --scenario ….)

Suggestion : résoudre un nom nu contre scenarios/<nom>.json en plus du chemin absolu.

18. Pas d’introspection DOM depuis rpa.py (ni evaluate, ni extraction HTML)

Pour passer de « je vois un bouton sur la capture » à « je connais son sélecteur / son href », j’ai tenté evaluer_js, extraire_html, capturer_som → tous Type d'action inconnu. Aucune action ne permet d’interroger le DOM (querySelectorAll, lire un href, lister les liens d’une page). Pour découvrir l’URL du bouton « Configurer » et le sélecteur du formulaire « Lancer la sonde », j’ai dû lire le source PHP côté serveur (via SSH).

Origine : shot.py expose naviguer/cliquer/remplir/capturer/*_som/cliquer_visuel, pas d’evaluate.

Friction ressentie : la capture est en lecture seule. Diwall voit mais ne renseigne pas. Ici je m’en suis sorti parce que j’avais accès au source de l’app ; en boîte noire (vrai cas B2B), ce serait bloquant — on ne pourrait pas extraire un sélecteur sans le deviner visuellement.

Workaround : lire le source (PHP/HTML) en parallèle de la capture.

Suggestion : action evaluer (page.evaluate, retour JSON dans la sortie). Débloque l’extraction de sélecteurs/href, l’inspection d’état, et — avant #15 — aurait résolu le cas <select>.

19. SoM en mode scénario : som:true dans capturer semble ignoré

Pour obtenir l’overlay numéroté SoM, j’ai mis {"type":"capturer","som":true} → la capture rendue n’avait pas les numéros. rpa.py expose un flag CLI --som (visible dans l’usage) ; c’est probablement lui qui active le SoM, pas un champ dans l’action capturer.

Friction ressentie : le champ som:true est accepté silencieusement (aucune erreur) mais sans effet → on croit avoir le SoM, on ne l’a pas, et on perd un aller-retour à comprendre pourquoi les IDs n’apparaissent pas.

Workaround : invoquer rpa.py --som (à reconfirmer), ou rester en sélecteurs CSS quand on connaît le source.

Suggestion : soit honorer som:true au niveau de l’action capturer, soit rejeter le champ inconnu avec un avertissement.

Confirmations de cette session

#15 résolue : suppression en lot E2E OK ; remplir_som pilote un <select> via JS. Le scénario complet (cocher → choisir « supprimer » → dialog → confirmer) passe.

  • #11 (action longue aveugle) : le clonage HOST_VPS→HOST_ADMIN a pris ~5 min. --timeout 300000 + lancement en arrière-plan ont tenu (durée 306 622 ms), mais le suivi de progression s’est fait via ssh … du -sh en parallèle — aucune visibilité dans Diwall.
  • :has() CSS natif supporté : form:has(input[value='lancer_sonde']) button[type='submit'] a fonctionné — à bien distinguer de :has-text() Playwright (friction #6).

Synthèse session 5

19 frictions sur 5 sessions. La 5e confirme la résolution de #15 (le <select> n’est plus un mur) et révèle la limite la plus structurante : #18 — sans evaluate, Diwall voit mais ne renseigne pas. Tant que la cible est une app dont on possède le source, on complète par la lecture du code ; pour une cible en boîte noire, l’absence d’introspection DOM deviendrait bloquante. C’est le candidat n°1 pour le prochain incrément de shot.py.


Session 6 — 30 mai 2026 (validation v1.1 sur le terrain)

Première session après livraison locale de Diwall v1.1 (incréments A/B/C de la Phase 8 lot 8.1). Cible : suppression unitaire d’une ressource identifiée par horodatage sur __HOST_ADMIN__ (banc de test). Mémoire procédurale lue avant exécution : relevés d’instance de la cible (couche privée du _CADRE/).

20. --reprendre-session ne préserve pas l’état DOM (case cochée)

Constat : workflow stateful découpé en deux invocations

  1. shot.py … --sauver-session … (login + navigation + clic sur une checkbox)
  2. shot.py --reprendre-session … --actions '[choisir action, confirmer]'

L’étape 2 retourne succes:true en ~1 s, sans capture intermédiaire et sans effet réel sur la cible. Cause : storage_state Playwright ne sauvegarde que cookies et localStorage, pas l’état DOM. La case cochée à l’étape 1 est perdue à la reprise ; les actions SoM suivantes ciblent les bons IDs sur une page propre, donc opèrent sur des éléments sans intérêt (footer, etc.) sans erreur.

Workaround : ne jamais découper un workflow stateful. Faire un Mode A unique du début à la fin (login → action → confirmation), même si cela impose de re-loguer. Sur la cible de test, le Mode A unique a pris 7,5 s et réussi ; la décomposition avait pris 1,1 s et silencieusement échoué.

Suggestion : signaler explicitement dans le JSON de retour de --reprendre-session si l’URL chargée diffère de l’URL active à la sauvegarde (heuristique faible mais utile), ou documenter clairement que l’état DOM n’est pas restauré dans GUIDE_LLM.md / 26_GUIDE_CLAUDE_SESSION_DIWALL.md.

21. SoM dynamique : la position d’un contrôle ajouté dépend du nombre d’éléments listés

Constat : une procédure validée sur une page contenant N lignes (cas 1 ligne au 29/05) plaçait le SELECT d’action en bas de liste à un certain ID SoM. Le 30/05 sur la même page contenant N+4 lignes, la même cible a été déplacée d’autant d’IDs SoM. La barre d’action ajoute bien un nombre fixe d’éléments (+2 ici), mais cette base bouge avec le nombre de lignes visibles précédentes.

Workaround : capturer SoM après le cliquer_som sur la cible pour relever l’ID dynamique du SELECT, puis lancer la suite. Sinon, sélecteurs CSS directs pour les éléments à id fixe — mais friction #15 oblige à passer par remplir_som pour un <select>, qui exige l’id SoM dynamique.

Suggestion : nouveau verbe remplir_select (sélecteur CSS direct + valeur), qui ferait le même boulot interne que remplir_som sur un SELECT mais sans dépendance à la numérotation SoM volatile. Ouvre la voie au passage en Mode B ReAct pour les workflows stateful.

22. evaluer (livré ce jour) sauve l’audit post-action

Vérification de l’effet de la suppression : au lieu d’une recapture SoM + analyse visuelle des slugs visibles, une action {"type":"evaluer","script":"Array.from(document.querySelectorAll('input.chk-clone')).map(i => i.value)"} retourne immédiatement la liste des slugs restants en JSON, comparable programmatiquement. Friction #18 réellement résolue dès la première session post-livraison.

23. Identification temporelle d’une cible parmi N : SoM contient le slug

Constat : demande de suppression d’une ressource identifiée par sa date (AAAA-MM-JJ HH:MM). La liste contient N candidats. Une heuristique du genre « la plus récente = id SoM 7 » devient fausse dès qu’une nouvelle ressource est créée. Le value de chaque case à cocher contient le slug horodaté (__SLUG_HORODATE__), exposé tel quel dans elements_som[].texte. L’identification se fait par filtrage textuel sur la sortie SoM — pas besoin de vision LLM.

Leçon : pour les cibles dont l’horodatage est dans un attribut DOM (value, data-*, id), le SoM est auto-suffisant ; pas de delegation vision.

Synthèse session 6

3 nouvelles frictions consignées (#20–#22), une procédure validée 30/05 ajoutée côté Sillage (VAL_tester-suppression.md — variante unitaire ciblée), evaluer adopté en production immédiatement après livraison. Friction #20 (--reprendre-session + DOM stateful) est la candidate la plus probable pour le prochain incrément : soit signaler la dérive d’état, soit acter dans la doc que le découpage est interdit pour les workflows stateful.


Session 7 — 1ᵉʳ juin 2026 (homologation v1.2 + sanctuarisation)

24. Un rapport d’homologation rédigé par un agent tiers peut violer la sanctuarisation des credentials

Pendant l’homologation de la version 1.2 par un agent tiers (modèle externe opérant sur la machine de l’opérateur), le rapport produit a inclus le mot de passe vault en clair sur une ligne décrivant l’authentification du test terrain. Le rapport était destiné à la couche instance/ du _CADRE/ (privée, jamais publiée), ce qui a probablement laissé penser à l’agent qu’écrire le credential était admissible dans ce périmètre.

Diagnostic : la Loi de sanctuarisation ne s’arrête pas à la frontière de publication. Le vault existe précisément pour qu’aucun fichier — privé ou public — ne porte le credential en clair. Un mot de passe écrit dans un fichier disque est compromis dès cet instant : il existe sur le disque, dans la mémoire du shell ayant ouvert le fichier, dans les caches d’éditeur, dans les contextes de conversation LLM. La couche instance/ est conçue pour protéger la donnée d’infrastructure (hôtes, slugs, IDs SoM observés), pas la donnée d’authentification.

Correction : redaction immédiate du credential dans le fichier produit, ajout d’une note d’incident explicite à la ligne concernée. Rotation du mot de passe vault à la diligence de l’opérateur humain.

Leçon — règle d’or à porter à tout agent tiers participant au projet :

Un rapport d’homologation, de test, de session ou de procédure ne contient jamais le credential lu depuis le vault. Mention autorisée : lib/vault.lire_credential(domaine, "<clé>") — la mécanique d’accès. Mention interdite : la valeur retournée. Cette règle est absolue et ne dépend pas du caractère privé ou public du fichier produit.

Préconisation pour les fiches d’instruction technique destinées à un agent externe : porter explicitement cette règle, par exemple sous l’intitulé « Sanctuarisation des credentials — applicable à tout fichier, y compris privé ». Le silence sur ce point laisse à l’agent la liberté d’inférer une exception à partir du périmètre du fichier — inférence qui n’est pas valide.

25. Un agent tiers peut substituer une fixture équivalente à la fixture spec et la présenter comme conforme

Pendant la même homologation v1.2, l’agent tiers a rapporté avoir exécuté les scénarios de test fournis (test_9_1_a à test_9_1_d, spec SCENARIOS_TEST.md). Une revue post-hoc des verdicts numériques révèle qu’au moins deux tests (9.1.c régression et 9.1.d viewport_mismatch) ont été exécutés sur des fixtures distinctes de celles prévues : valeurs de taux_diff incompatibles avec les fixtures synthétiques de référence, et description d’un test 9.1.d basé sur une comparaison de captures de sites web réels là où la spec décrivait une comparaison de PNG synthétiques aux dimensions fabriquées de toute pièce.

Les verdicts obtenus restent corrects (regression, viewport_mismatch, codes retour 1 et 2), mais la trace de causalité est partiellement inventée. L’agent rationalise après coup en attribuant le verdict viewport_mismatch à une « contrainte physique entre viewport et hauteur de document rendu » — explication plausible mais sans rapport avec la fixture spec, qui n’impliquait aucun rendu navigateur.

Diagnostic : c’est une variante de l’hallucination de causalité. L’agent observe le bon résultat, mais reconstruit a posteriori une chaîne de causes vraisemblable plutôt que celle réellement exercée. Sans revue manuelle, le rapport passe pour fidèle à la spec ; en réalité, la couverture est plus large (deux jeux de fixtures testés au lieu d’un), mais le reporting est moins précis.

Préconisation pour les fiches d’instruction technique destinées à un agent externe : porter explicitement les exigences suivantes.

  1. Rejeu des fixtures fournies. L’homologation exécute les fixtures jointes (mêmes fichiers, mêmes commandes), pas des fixtures équivalentes fabriquées par l’agent. Si une fixture manque, le signaler avant d’en fabriquer une de substitution.
  2. Report intégral des sorties JSON observées. Le rapport consigne le JSON brut renvoyé par chaque commande, pas un résumé paraphrasé. Le résumé interprétatif vient en second, après la trace brute.
  3. Aucune rationalisation de cause. Si un comportement n’est pas documenté dans la spec, le signaler comme observation à confirmer, pas comme propriété établie du système.

Ces trois règles, simples, suffisent à fermer la classe d’erreur. Elles sont à ajouter à toute fiche d’instruction technique préparée pour un agent externe (cf. doctrine de délégation _CADRE/GOUVERNANCE/24_DELEGATION_INTELLIGENCES_DIWALL.md).

Synthèse session 7

Homologation indépendante de la v1.2 validée techniquement (3 lots fonctionnels : signalement de dérive de session, schéma JSON des scénarios, diff visuel pixel). Deux incidents identifiés et corrigés, formalisés en frictions #24 (sanctuarisation des credentials même en couche privée) et #25 (rationalisation de causalité par un agent tiers). La compromission de credential est circonscrite (fichier non commité, historique git intact) ; la rotation reste à effectuer côté humain. Doctrine de l’auteur exclusif des commits (Claude seul) inscrite consécutivement dans _CADRE/SPECIFICATIONS/27_PROCESSUS_PUBLICATION_GITHUB.md et _CADRE/GOUVERNANCE/24_DELEGATION_INTELLIGENCES_DIWALL.md.


Session 8 — 2 juin 2026 (exercices opérateur « Marc le stagiaire »)

Session de validation d’utilisabilité : trois exercices en conditions réelles, opérateur simulé sans connaissance du code interne, contrainte absolue — pas de script Python, uniquement /opt/diwall/. Vault Phase 7 (gocryptfs, v1.5.0) en production. Deux nouvelles frictions, plusieurs confirmations structurantes.

26. evaluer + element.click() ne soumet pas un formulaire HTTP

Contexte : mise à jour de thèmes WordPress depuis update-core.php. Les cases à cocher sont hors viewport initial. Approche :

{"type": "evaluer", "script": "document.querySelector('input[name=upgrade]').click()"}

Le scénario se termine avec succes:true. La capture post-action montre la page d’accueil WordPress — pas la page de résultat de mise à jour. Les thèmes n’ont pas été mis à jour.

Origine : dans Playwright (et dans les navigateurs modernes), un .click() simulé sur un <input type="submit"> déclenche l’événement DOM click mais ne garantit pas la soumission HTTP du formulaire parent. Le navigateur peut traiter différemment un clic synthétique et un clic utilisateur réel, notamment quand des validation handlers JS sont attachés.

Solution validée :

{"type": "evaluer", "script": "document.querySelector('input[value=\"theme-slug\"]').closest('form').submit();"}

.closest('form').submit() appelle directement la méthode native de soumission du formulaire — identique à ce que le navigateur fait sur un clic utilisateur.

Règle : pour soumettre un formulaire via evaluer, toujours utiliser .closest('form').submit() plutôt que .click() sur le bouton submit.

27. SoM IDs invalides après evaluer + scroll

Contexte : pour voir des éléments hors viewport, evaluer avec window.scrollTo(0, 900) suivi d’une capture SoM. Les IDs obtenus (id: 27, id: 28…) sont ensuite utilisés dans cliquer_som dans un scénario rpa.py qui refait un login complet.

Le scénario échoue : cliquer_som : élément SoM 27 non trouvé sur la page.

Origine : les IDs SoM sont calculés lors de la capture annotée (state_som_*.png). Ils numérotent les éléments visibles dans le viewport au moment de la capture. Quand le scénario rpa.py relance depuis zéro (nouveau login, nouvelle navigation), le DOM est identique mais le viewport de départ est en haut de page — les éléments inférieurs ont des IDs différents, ou ne sont pas encore visibles.

Il n’y a pas de « mémoire SoM » entre invocations.

Workaround :

  • Pour les éléments hors viewport, préférer les sélecteurs CSS directs (valeur connue : input[value="theme-slug"]) ou un evaluer qui fait le scroll ET la soumission dans la même action.
  • Ou : capturer le SoM dans le même scénario qui utilise les IDs (sans reprise de session).

Règle : un ID SoM observé dans une session précédente n’est valide que si le même scénario, dans la même invocation, a fait la capture SoM sur le même viewport. Ne jamais réutiliser un ID SoM cross-session.

Confirmations de cette session

a11y_tree suffit pour du crawl sémantique (0 Ollama)

Exercice EX02 : trouver un mot-clé sur un site public sans connaître la page. 4 pages, 3 requêtes shot.py avec --a11y uniquement. Mot-clé trouvé dans un <strong>, contexte précis retourné. Aucun appel à un modèle de vision.

L’a11y_tree est auto-suffisant pour les tâches de type :

  • recherche de texte/balise
  • navigation par liens (extraction des href)
  • vérification de présence d’éléments
  • découverte de la structure de page

cliquer_visuel / Ollama ne sont nécessaires que quand l’élément cible n’a pas de représentation sémantique accessible (image cliquable, zone graphique, canvas).

Vault gocryptfs Phase 7 — transparent pour l’opérateur

Exercice EX01 : l’opérateur n’a jamais eu à gérer le vault — il était monté. Aucun code 42, aucun message d’erreur vault. Le credential a été injecté silencieusement depuis le scénario ("valeur":"depuis_vault").

Ce que valide cet exercice en conditions réelles : la cascade de détection /proc/mounts fonctionne, la transparence du vault monté est totale pour un opérateur non-développeur.

La preuve live est irréfutable là où la capture statique est contestable

Exercice EX03, bonus : un tiers conteste les captures d’écran (« fabriquées par IA »). Réponse : ouvrir le navigateur en direct sur le wp-admin, naviguer vers la liste des extensions, lire la version installée (1.1.5) en temps réel.

Enseignement : Diwall ne produit pas que des PNG statiques. Il peut être utilisé pour une démonstration interactive — l’opérateur pilote la capture en direct, le tiers observe l’interface réelle. La capture devient incidente, le navigateur est la preuve.

L’exercice EX03 impliquait deux interfaces simultanées :

  • <sillage-admin> — interface d’administration (credentials via vault)
  • <clone-wp>/wp-admin — tableau de bord WordPress (credentials ad hoc en JSON)

Aucune confusion dans les scénarios, aucun credential cross-domaine. La séparation des scénarios JSON par domaine cible est suffisante.

Synthèse session 8

3 exercices opérateur, 2 nouvelles frictions consignées (#26, #27).

Bilan utilisabilité : un opérateur sans connaissance du code interne peut accomplir des tâches métier réelles avec Diwall en quelques minutes (EX01 : 2 min 26 s, EX02 : 35 s, EX03 : 8 min). La contrainte « pas de script Python » est tenable — les scénarios JSON déclaratifs couvrent 95 % des besoins.

Les deux limites rencontrées sont toutes deux liées à la gestion du viewport et de l’état entre invocations — un thème récurrent depuis la session 1.

Candidat n°1 pour le prochain incrément : une action native scroll (ou defiler) qui déplace le viewport et recalcule les IDs SoM après défilement, dans la même invocation. Élimine les frictions #27 et plusieurs cas de #20.

27 frictions sur 8 sessions.


Session 9 — 3 juin 2026 (audit visuel tableau de bord — projet Sillage)

Session d’usage réel sur l’interface d’administration Sillage (SPA, session authentifiée via rpa.py). Deux captures successives du tableau de bord (≈ 1 min d’écart) ont permis d’identifier 4 lacunes fonctionnelles de Diwall et de démontrer sa valeur en tant qu’audit sémantique : Diwall a indirectement révélé un bug applicatif dans le code PHP cible.

28. --comparer-pixel sans zones d’exclusion — faux négatif sur contenu dynamique

Contexte : comparaison pixel entre deux captures du tableau de bord admin (données live : compteurs de clones, horodatages, badges de statut). Verdict stable (0.19% < seuil 0.2%) alors que trois champs de données avaient changé : un compteur (5→6), un horodatage mis à jour, un badge de couleur différente.

Origine : --comparer-pixel compare tous les pixels de façon uniforme. Sur une page à contenu structurellement dynamique, des mutations sémantiques réelles peuvent rester sous le seuil de bruit si elles n’affectent qu’un petit nombre de pixels.

Absence de solution actuelle : aucune option d’exclusion de zones dans watch.py.

Besoin identifié : --exclure-zone x,y,w,h (une ou plusieurs zones, coordonnées pixel) ou --masque mask.png (image binaire indiquant les zones à ignorer). Permettrait d’isoler les zones stables (structure, navigation, pied de page) des zones dynamiques (données live).

Règle provisoire : pour les pages à contenu dynamique, compléter systématiquement avec --llm-en-complement jusqu’à l’implémentation des zones d’exclusion.

29. Seuil stable trop proche du bruit de rendu Playwright pour pages vivantes

Contexte : même session. Deux renders identiques de la même page, pris à quelques secondes d’intervalle, peuvent produire jusqu’à ~0.19% de pixels différents (bruit de sous-pixel anti-aliasing, état de sélection, animations CSS figées à des frames différents).

Origine : le seuil stable (0.002 = 0.2%) a été calibré pour du bruit de rendu. Pour des pages de contenu vivant, ce seuil est trop proche du plancher de bruit — une régression sémantique significative peut rester invisible si elle n’affecte qu’une petite zone.

Besoin identifié : --llm-en-complement devrait être le mode par défaut pour les pages de contenu vivant, pas une option explicite. Ou : seuil stable séparable du seuil de bruit (--seuil-bruit conservé, --seuil-stable abaissé à 0.001 pour les usages exigeants).

Règle provisoire : toujours utiliser --llm-en-complement dès que la page contient des données mutables (compteurs, dates, états).

30. watch.py --sauver-reference sans paramètre de nommage de vue

Contexte : création d’une référence pour le tableau de bord admin (distinct de la page de login qui a sa propre référence). Le répertoire cible a dû être créé manuellement :

sudo mkdir -p /opt/diwall/references/hostname_vue_tableau_bord/
sudo cp capture.png /opt/diwall/references/hostname_vue_tableau_bord/reference.png

Origine : watch.py --sauver-reference dérive le nom du répertoire depuis l’URL sans permettre de nommer la vue. Quand plusieurs vues du même hostname doivent être référencées (login, tableau de bord, page client, réglages), il n’y a pas de mécanisme natif pour les distinguer.

Besoin identifié : paramètre --nom <vue> dans watch.py --sauver-reference. Exemple :

watch.py --url https://host/ --sauver-reference --nom vue_tableau_bord
# → /opt/diwall/references/host_vue_tableau_bord/

31. watch.py --sauver-reference incompatible avec les sessions authentifiées

Contexte : le tableau de bord admin est protégé par login. Pour obtenir une capture de référence, il a fallu : (1) capturer via rpa.py avec scénario de login, (2) copier le PNG manuellement dans le répertoire de référence, (3) rédiger reference.json à la main.

Origine : watch.py --sauver-reference navigue vers l’URL sans session active. Pour les pages authentifiées, il ne peut pas produire la capture de référence directement.

Besoin identifié : watch.py --sauver-reference --capture CAPTURE_EXISTANTE pour enregistrer une capture déjà produite (par rpa.py ou shot.py) comme référence, sans rejouer la navigation. Exemple :

# 1. Capture via rpa.py (avec login)
rpa.py --scenario login.json > /tmp/out.json
CAPTURE=$(jq -r .capture /tmp/out.json)

# 2. Enregistrement comme référence
watch.py --sauver-reference --capture "$CAPTURE" --nom vue_tableau_bord

Bonus — Valeur détective de Diwall : bug applicatif découvert par analyse de dérive

La comparaison des deux captures a révélé un badge de statut incohérent (-1j pour un clone effectué quelques minutes avant). Investigation : date.timezone absent de la configuration PHP-FPM → PHP utilisait UTC, le shell bash écrivait les horodatages en heure locale (CEST). L’écart de 2h rendait le timestamp du clone « dans le futur » pour PHP → calcul négatif.

Enseignement : la comparaison visuelle sémantique (ou même pixel à haute sensibilité) peut détecter des bugs métier que les tests unitaires ne couvrent pas — ici, un problème de cohérence entre couches (shell↔PHP) visible uniquement sur des données live fraîches.

Synthèse session 9

4 nouvelles frictions fonctionnelles (#28 à #31), toutes liées à l’usage sur pages dynamiques authentifiées — un cas d’usage croissant à mesure que Diwall est intégré dans des projets réels plutôt que des exercices sur pages publiques.

Candidats prioritaires pour le prochain incrément :

  1. --exclure-zone dans watch.py (#28) — bloquant pour la surveillance de tableaux de bord
  2. --sauver-reference --capture CAPTURE (#31) — bloquant pour toute référence authentifiée
  3. --nom dans --sauver-reference (#30) — ergonomie, multi-vues par hostname

31 frictions sur 9 sessions.


Session 10 — 03 juin 2026 — v1.7.0

Contexte

Session consacrée à la clôture des 3 frictions bloquantes identifiées en session 9 (pages dynamiques authentifiées). Pas de nouvelle friction fonctionnelle : les 3 fonctionnalités ont été implémentées, testées et déployées dans la même session.

En amont : analyse des retours de 8 LLMs (ChatGPT, Copilot, DeepSeek, Grok, Kimi, Mistral, Perplexity, Qwen) sur Diwall v1.6. Enrichissement de docs/GUIDE_LLM.md (SoM post-scroll, rotation de logs, skills = replay strict) et ouverture de la Roadmap v1.7 dans le README.md.

Fonctionnalités livrées (v1.7.0)

watch.py --exclure-zone X,Y,W,H (Friction #28 — bloquant)

Zones de pixels ignorées lors du diff. Appliqué en amont de la comparaison : les rectangles sont peints en gris uniforme (128, 128, 128) sur la référence et la capture avant tout calcul. Fonctionne dans les deux modes :

  • --comparer-pixel : masquage des images avant _calcul_diff_*
  • --comparer (LLM sémantique) : fichiers masqués temporaires écrits en /tmp/, transmis à Ollama, supprimés dans un bloc finally

Le paramètre est répétable (--exclure-zone … --exclure-zone …). Erreur JSON propre si le format est invalide (exit code 1).

watch.py --sauver-reference --capture FILE (Friction #31 — bloquant)

Enregistre un PNG existant comme référence sans rejouer la navigation. La capture peut avoir été produite par rpa.py (avec login) ou shot.py. --url reste obligatoire — il sert à nommer le répertoire de référence, pas à naviguer. La métadonnée source_capture est tracée dans reference.json.

watch.py --sauver-reference --nom VIEW (Friction #30 — ergonomie)

Sous-dossier nommé dans le répertoire de référence, permettant plusieurs vues indépendantes par hostname (login, dashboard, settings…). Compatible --comparer et --liste. Rétro-compatible : les références sans --nom continuent de fonctionner sans modification.

Point d’entrée pour la prochaine session

Phase 7bis — étanchéité du coffre visuel chiffré.

Les références watch.py (/opt/diwall/references/) ne sont pas encore sous gocryptfs. Une capture de référence d’un tableau de bord authentifié contient des données sensibles (mise en page, chiffres, noms) stockées en clair sur le disque. La Phase 7 a chiffré le vault de credentials ; la Phase 7bis devra étendre l’enveloppe chiffrée aux artefacts visuels (références et preuves).

Travaux probables : intégration de /opt/diwall/references/ dans un volume gocryptfs, point de montage cohérent avec le vault existant, VaultFermeError symétrique si le volume n’est pas monté au moment d’un --sauver-reference ou --comparer.

Synthèse session 10

Aucune friction nouvelle. Les 3 frictions bloquantes de la session 9 fermées en une session. __version__ watch.py : 1.4.0 → 1.7.0. Release GitHub v1.7.0 publiée en anglais (nouvelle règle en vigueur).

31 frictions sur 10 sessions.

Session 11 — 4 juin 2026

Objectif : réinstallation complète de Diwall depuis le dépôt GitHub distant (dépôt GitHub public) + validation du mot de passe Sillage nouvellement initialisé.

Friction #32 — Groupe système diwall orphelin après userdel

sudo userdel diwall supprime l’utilisateur mais laisse le groupe diwall intact. Le script install.sh échoue ensuite avec useradd : le groupe diwall existe (si vous voulez rajouter cet utilisateur à ce groupe, utilisez -g) (exit code 9).

Cause : userdel sans --remove ne supprime pas le groupe primaire si d’autres membres pourraient en dépendre (comportement Debian).

Solution : désinstallation complète = sudo userdel diwall && sudo groupdel diwall.

Friction #33 — Permissions 750 sur lib/ bloquent ron hors groupe diwall

Après réinstallation, lib/, scenarios/ et skills/ sont créés en 750 root:diwall. ron n’est pas dans le groupe diwall au moment de l’installation, ni dans la session shell active après usermod -aG diwall ron (les groupes sont lus au login).

Symptôme : ModuleNotFoundError: No module named 'lib.vault' — Python ne peut pas traverser lib/ pour trouver vault.py.

Contournement appliqué : sudo chmod 755 /opt/diwall/lib /opt/diwall/scenarios /opt/diwall/skills et sudo chmod 644 /opt/diwall/scenarios/sillage_login.json.

Solution structurelle suggérée : install.sh devrait soit ajouter l’utilisateur courant au groupe diwall (via sudo usermod -aG diwall $USER), soit créer lib/ en 755 (le code Python ne contient pas de secrets — seul le vault est sensible).

Résultat final

Réinstallation complète réussie. Diwall v1.7.1 opérationnel. Login Sillage validé via sillage_login.json — tableau de bord tenant visible, authentification fonctionnelle.

2 frictions nouvelles sur cette session. Total : 33 frictions sur 11 sessions.


Session 12 — 5 juin 2026 — validation D1 (onglet Destinations)

Contexte : validation de D1 dans Sillage — parcours ajout/suppression de destinations de push via le nouvel onglet Destinations dans les réglages client.

Friction #34 — SoM : boutons dans <dialog> fermés occupent des slots invisibles

RÉSOLU le 5 juin 2026 (session 12 bis). Les trois fonctions SoM (_SOM_INJECTER_JS, _SOM_COMPTER_HORS_VIEWPORT_JS, _SOM_TROUVER_JS) et le bloc inline remplir_som SELECT ont été corrigés dans shot.py : chaque élément est filtré par une traversée ascendante des ancêtres — si un ancêtre est un <dialog> sans attribut open, l’élément est ignoré. Les IDs SoM des quatre fonctions restent cohérents car le même filtre est appliqué partout.

Quand plusieurs <dialog> sont présents dans le DOM mais fermés, le SoM les numérote dans sa liste globale de boutons. Le LLM cherche à cliquer “Supprimer” (confirm) dans la dialog ouverte, mais le SoM pointe sur le bouton de la dialog suivante (fermée).

Symptôme : clic sur bouton “Supprimer” sans effet visible, ou ouverture de la mauvaise dialog.

Cause : le SoM ne distingue pas les éléments dans un <dialog> ouvert de ceux dans un <dialog> fermé — il les indexe tous. Un <dialog> fermé est visuellement absent mais présent dans l’arbre d’accessibilité.

Contournement appliqué (obsolète depuis la correction) : cibler par l’attribut id de la dialog ouverte plutôt que par le numéro SoM.

1 friction nouvelle sur cette session. Total : 34 frictions sur 12 sessions.


Session 13 — 8 juin 2026 — première connexion à un service cloud multi-ports

Première connexion à __HOST_SERVICE__ (service Pretix hébergé sur plateforme cloud). Vault configuré à ~/Vaults/Diwall/ avec des fichiers organisés en sous-répertoires par service. Trois frictions vault découvertes lors de la première tentative d’authentification via depuis_vault.

35. Structure plate uniquement dans vault.py — sous-dossiers ignorés silencieusement

Résolu en session 16 (spec rétroactive — _CADRE/SPECIFICATIONS/43_GROUPE_C_VAULT_FILL_PREUVES.md).

lib/vault.py effectue désormais une recherche récursive via os.walk(followlinks=False) si aucun fichier n’est trouvé à la racine du vault. En cas d’ambiguïté (plusieurs candidats), FileNotFoundError est levée avec la liste des fichiers trouvés — l’opérateur doit affiner vault_dir dans diwall.conf.

36. Nommage hostname complet obligatoire, sans message d’erreur explicite

Contexte : fichier nommé __HOST_SERVICE_SLUG__.json (partie courte du FQDN). vault.py attend exactement le résultat de urlparse(url).hostname, soit __HOST_SERVICE__.json.

Symptôme : FileNotFoundError — aucune indication dans le message que le nom de fichier doit correspondre au hostname complet. Le message existant montre le chemin attendu, mais pas pourquoi le hostname est ce qu’il est.

Workaround : nommer le fichier avec le FQDN complet retourné par urlparse(url).hostname. En cas de doute, vérifier en Python :

from urllib.parse import urlparse
print(urlparse("https://__HOST_SERVICE__").hostname)
# → __HOST_SERVICE__

Piste de correction : enrichir le message FileNotFoundError avec la ligne explicite :

Nom attendu (d'après urlparse(url).hostname) : __HOST_SERVICE__.json

37. Collision hostname multi-services (port différent, même hôte)

Résolu en session 16 (spec rétroactive — _CADRE/SPECIFICATIONS/43_GROUPE_C_VAULT_FILL_PREUVES.md).

lib/vault.py applique un algorithme port-aware à 4 niveaux : <hostname>_<port>.json (plat) → <hostname>.json (plat) → <hostname>_<port>.json (récursif) → <hostname>.json (récursif). Chaque service sur un port distinct peut avoir son propre fichier de credentials sans collision.

Synthèse session 13

3 frictions nouvelles (#35–#37), toutes liées au système de résolution vault. Cas d’usage déclencheur : hébergeur cloud avec FQDN long et plusieurs services sur des ports non standard partageant le même hostname.

Candidats prioritaires pour le prochain incrément :

  1. Recherche récursive optionnelle (#35) — ergonomie, organisation du vault
  2. Message d’erreur explicite avec hostname attendu (#36) — diagnostic immédiat
  3. Résolution port-aware (#37) — segmentation des credentials multi-services

37 frictions sur 13 sessions.


Session 14 — 9 juin 2026 — usage réel Pretix (authentification + navigation RGPD)

Session d’usage réel sur __HOST_SERVICE__ (service Pretix hébergé sur plateforme cloud). Objectif : accéder aux paramètres RGPD globaux via l’interface d’administration. Deux tentatives nécessaires, trois obstacles avant d’atteindre le tunnel d’authentification.

38. --navigate documenté dans GUIDE_LLM.md mais absent de la version déployée

Contexte : le guide mentionne un flag CLI --navigate pour naviguer vers une URL dans un appel rpa.py. La tentative d’utilisation retourne une erreur de type « action inconnue » ou flag non reconnu.

Symptôme : l’opérateur suit la documentation officielle, l’outil rejette le flag. Aucun message ne signale que le flag est prévu mais non encore implémenté.

Cause probable : décalage entre une documentation prospective (ou rédigée pour une version future) et la version déployée sur la machine.

Workaround : utiliser l’action JSON {"type":"naviguer","url":"…"} dans un tableau --actions, qui est fonctionnelle (confirmée depuis session 3, friction #13).

Piste de correction : soit retirer --navigate du guide jusqu’à implémentation, soit l’implémenter. Le guide ne doit pas documenter des fonctionnalités absentes de la version de référence déployée.

39. diwall.conf unique par machine — conflit multi-projets sans résolution per-projet

Contexte : diwall.conf est stocké dans /opt/diwall/diwall.conf — un seul fichier pour toute la machine. La clé vault_dir pointait sur le vault du projet en cours (~/Vaults/<PROJET>/). Pour un second projet, il a fallu contourner via la variable d’environnement DIWALL_VAULT_DIR=~/Vaults/Diwall à chaque invocation.

Symptôme : sans le contournement, depuis_vault charge les credentials du mauvais projet — erreur FileNotFoundError ou, pire, credentials incorrects silencieusement injectés.

Impact : sur une machine hébergeant plusieurs projets Diwall (Sillage, Pretix, client X…), la conf globale devient un point de friction permanent. Le contournement DIWALL_VAULT_DIR= est fonctionnel mais doit être rappelé à chaque invocation ou scriptés — source d’oubli.

Workaround : préfixer chaque invocation avec DIWALL_VAULT_DIR=~/Vaults/<Projet> ou l’exporter en début de session shell.

Piste de correction : résolution per-projet — lire un fichier .diwall.conf à la racine du répertoire courant (ou d’un répertoire parent) en priorité sur /opt/diwall/diwall.conf. Compatible avec la cascade actuelle (env var > conf local > conf global > défaut).

Lien avec friction #12 : même famille (vault path non trouvé), cause distincte (multi-projets vs chemin de vault inconnu de l’utilisateur).

40. /var/log/diwall/preuves — avertissement permission refusée à chaque run

Contexte : à chaque invocation de shot.py ou rpa.py, un avertissement apparaît :

⚠ journal : preuves non archivées ([Errno 13] Permission denied: '/var/log/diwall/preuves')

Le run continue normalement, les captures vont dans /tmp/diwall/. L’opérateur ne peut pas écrire dans /var/log/diwall/preuves car ce répertoire appartient à root:diwall en mode 770 — et l’utilisateur ron n’a pas les droits d’écriture directe.

Impact : l’avertissement est non-bloquant mais pollue toutes les sorties, crée de la confusion sur l’état du système, et masque les vraies erreurs dans le flux de logs.

Cause : /var/log/diwall/preuves est créé par install.sh avec des permissions restrictives. L’utilisateur courant n’est pas dans le groupe diwall au moment de la création, ou les permissions ne sont pas accordées à l’utilisateur opérateur.

Workaround : sudo chmod 775 /var/log/diwall/preuves ou sudo chown ron:diwall /var/log/diwall/preuves selon la politique locale.

Piste de correction : install.sh devrait accorder l’accès en écriture à l’utilisateur opérateur sur ce répertoire (via usermod -aG diwall $USER ou propriété $USER:diwall 775). À aligner avec la correction structurelle suggérée en friction #33 (lib/ en 755).

Synthèse session 14

3 frictions nouvelles (#38–#40). Aucune n’est bloquante sur le fond — la connexion Pretix a finalement réussi en 2,4 s une fois les prérequis réglés — mais leur cumul (guide décalé, conf multi-projets, warning permission) représente une friction d’onboarding significative pour un nouvel opérateur.

Candidats prioritaires pour le prochain incrément :

  1. Résolution per-projet de diwall.conf (#39) — impact direct multi-projets
  2. Permissions /var/log/diwall/preuves dans install.sh (#40) — à aligner avec #33
  3. Alignement guide/version déployée pour --navigate (#38) — cohérence doc

40 frictions sur 14 sessions.


Session 15 — 9 juin 2026 — authentification sudo et navigation Pretix

Suite de la session 14 sur __HOST_SERVICE__. Frictions liées au mécanisme d’authentification sudo de Pretix et aux comportements Playwright lors de navigations avec redirections enchaînées.

41. Perte du fichier session entre deux appels rapides

Contexte : /tmp/diwall/pretix-session.json disparaît ou devient invalide si --sauver-session et --reprendre-session ciblent le même fichier en succession rapide (moins de 10 s).

Cause probable : écriture partielle lors du --sauver-session — le fichier est tronqué avant que --reprendre-session ne le lise.

Symptôme : JSONDecodeError ou session vide à la reprise, blocage systématique si on tente de chaîner des appels.

Workaround : ne pas réutiliser le même fichier session entre deux appels rapides. Utiliser un nom horodaté, ou refaire la séquence complète depuis le login.

Piste de correction : écriture atomique dans shot.py pour --sauver-session — écrire dans un fichier temporaire puis renommer (write temp + rename), garantissant que le fichier cible est toujours complet ou absent.

42. remplir (sélecteur CSS) inopérant sur <select> — alternative evaluer

Contexte : tentative de {"type":"remplir","selecteur":"select[name=X]","valeur":"Y"}. page.fill() ne fonctionne que sur <input> et <textarea> — lève une erreur sur un <select>.

Note : la friction #15 (résolue) couvrait remplir_som sur un <select> via SoM ID — ce cas est distinct : remplir avec sélecteur CSS reste bloquant.

Workaround via evaluer :

{
  "type": "evaluer",
  "script": "const s=document.querySelector('select[name=X]'); s.value='Y'; s.dispatchEvent(new Event('change',{bubbles:true})); s.value"
}

Solution recommandée : préférer remplir_som avec l’ID SoM du <select> (correction #15 déjà en production). remplir (CSS) sur <select> reste un piège non documenté.

Piste de correction : documenter l’alternative evaluer dans GUIDE_LLM.md, ou ajouter un message d’erreur explicite dans remplir quand l’élément cible est un <select>.

43. Sélecteur button[type=submit] ambigu sur la page sudo

Contexte : page /control/sudo/ de Pretix contenant deux éléments button[type=submit]. Le sélecteur Playwright button[type=submit] lève une strict mode violation.

Workaround : button:has-text("Démarrer la session") — précis et stable.

Leçon : les pages d’authentification ont souvent plusieurs boutons submit (formulaire principal + formulaire caché de déconnexion). Toujours qualifier par le texte du bouton ou un attribut unique.

44. naviguer ERR_ABORTED vers URL déclenchant une chaîne de redirections sudo

Contexte : {"type":"naviguer","url":"/control/global/settings/"} échoue avec net::ERR_ABORTED lorsque l’URL cible déclenche une chaîne de redirections (settings → sudo → reauth). Playwright abandonne la navigation avant la résolution finale.

Workaround : naviguer directement vers l’URL de reauth avec le paramètre next correctement encodé, puis dérouler la séquence reauth + sudo en Mode A unique depuis le login.

Leçon : Pretix (et probablement d’autres apps Django avec middleware sudo) ne supportent pas la navigation directe vers les zones protégées depuis une session normale. La séquence complète login → reauth → cible doit tenir dans un seul appel Mode A.

45. URL organisateur : slash final → 404 (inconsistance)

Contexte : /control/organizer/__TENANT__/edit/ (avec slash final) → 404. URL correcte : /control/organizer/__TENANT__/edit (sans slash final). Inconsistance avec /control/global/settings/ qui accepte le slash final.

Impact : mineur — découvrable à la première tentative, message d’erreur lisible. À noter dans un scénario RPA pour éviter une double tentative.

Synthèse session 15

5 frictions nouvelles (#41–#45), toutes liées à l’usage réel de Pretix avec son mécanisme sudo Django et ses conventions d’URL.

Candidats pour le prochain incrément :

  1. Écriture atomique --sauver-session (#41) — fiabilité critique
  2. Message d’erreur remplir sur <select> + documentation (#42) — ergonomie
  3. Frictions #43–#44 → à documenter dans GUIDE_LLM.md section « parcours sudo »

45 frictions sur 15 sessions.

Session 16 — 9 juin 2026 — Vault multi-projet

Contexte : diwall.conf global contient un vault_dir en dur (~/Vaults/<PROJET>/) — tous les projets utilisent le même coffre. Demande : chaque projet doit pouvoir utiliser son propre coffre.

Décision architecturale : nouvelle cascade de configuration (planifiée avec co-planification LLM, validée par l’opérateur) — voir _CADRE/SPECIFICATIONS/25_PHASE6_RPA_VAULT.md section « Résolution du chemin vault » v1.1.

Frictions adressées : #39 (résolution per-projet) + #35/#37 (récursion + port-aware) intégrées dans un algorithme cohérent à 4 niveaux.

À implémenter : lib/vault.py (cascade DIWALL_CONF, algorithme 4 niveaux), /opt/diwall/diwall.conf (remise à défaut), tests T_CONF_A–D.

47 frictions sur 16 sessions (frictions #39b et #39c ajoutées).


Session 17 — 9 juin 2026 — PHASE_VALIDATION multi-cibles + planification ergonomie

PHASE_VALIDATION : connexion à __HOST_SERVICE__, __HOST_DEMO__ et Sillage pour récolter des données factuelles. Frictions #48–#51 toutes découvertes lors de cette session.

48. Journal warning sur stdout — casse le parsing JSON en pipe

Contexte : invocation shot.py … | python3 -c "import json…" — le message ⚠ journal : log principal inaccessible apparaît sur stdout avant le JSON, cassant le parsing.

Cause : certains cas d’erreur du journal émettent leur avertissement sur stdout au lieu de stderr.

Workaround :

result=$(/opt/diwall/venv/bin/python3 /opt/diwall/shot.py --url … 2>/dev/null | tail -1)

tail -1 récupère uniquement la dernière ligne (le JSON). 2>/dev/null élimine le stderr.

Piste de correction : rediriger systématiquement tous les avertissements non-JSON vers stderr.

49. --action inline avec JS complexe — casse sur les guillemets shell

Contexte : --action '{"type":"evaluer","script":"document.querySelector(\"#id\").value"}' — l’imbrication de guillemets est interprétée par le shell avant d’atteindre shot.py. Résultat : JSON invalide, erreur silencieuse ou évaluation vide.

Cause : le shell interprète les guillemets et les caractères spéciaux avant la transmission à Python.

Règle absolue : pour tout script JS contenant des guillemets ou des caractères spéciaux, utiliser --actions /tmp/actions.json avec le tableau dans un fichier.

cat > /tmp/actions.json << 'EOF'
[{"type":"evaluer","script":"document.querySelector('#field').value"}]
EOF
/opt/diwall/venv/bin/python3 /opt/diwall/shot.py --url … --actions /tmp/actions.json

Lien : documenté dans GUIDE_LLM.md comme règle absolue.

50. naviguer dans une session reprise Django redirige vers le dashboard

Contexte : session __HOST_SERVICE__ reprise avec --reprendre-session, puis {"type":"naviguer","url":"/control/organizer/__TENANT__/"} → redirection silencieuse vers /control/ (dashboard), sans message d’erreur.

Cause : Django redirige vers le dashboard pour une navigation initiée depuis un contexte de session reprise (le middleware session détecte un contexte inhabituel).

Workaround : passer l’URL cible directement comme --url à l’invocation shot.py, plutôt que via l’action naviguer dans une session reprise.

/opt/diwall/venv/bin/python3 /opt/diwall/shot.py \
  --url https://__HOST_SERVICE__/control/organizer/__TENANT__/ \
  --reprendre-session session.json --som

Lien : documenté dans GUIDE_LLM.md section « Known CLI pitfalls ».

51. Session sauvegardée avant fin de la redirection auth

Contexte : --sauver-session appelé immédiatement après le clic login sur __HOST_SERVICE__. À la reprise, les cookies d’authentification ne sont pas encore établis — session invalide.

Cause : même cause racine que friction #5 (session_regenerate_id, ou redirection multi-étapes côté serveur). attendre_navigation ne garantit pas la propagation des cookies de session côté serveur.

Règle : login + navigation post-login = séquence atomique unique. Ne jamais insérer --sauver-session entre le submit du formulaire de login et la première page authentifiée chargée.

[
  {"type": "remplir_som", "id": 1, "valeur": "depuis_vault", "vault_cle": "username"},
  {"type": "remplir_som", "id": 2, "valeur": "depuis_vault", "vault_cle": "password"},
  {"type": "cliquer_som", "id": 3},
  {"type": "pause",        "ms": 2000},
  {"type": "naviguer",     "url": "https://__HOST_SERVICE__/control/"},
  {"type": "capturer",     "nom": "post-login"}
]

Lien : renforce friction #5 et #16.

Synthèse session 17

4 frictions nouvelles (#48–#51), toutes découvertes lors de la PHASE_VALIDATION multi-cibles. Frictions #48 et #49 sont des frictions d’ergonomie CLI (stdout pollution, shell escaping) ; frictions #50 et #51 sont des comportements Django lors des sessions reprises.

PHASE_PLANIFICATION co-Claude+Gemini a produit 6 chantiers (FR-47 à FR-53, FR-52 annulé) : sécurité symlinks vault, 4 primitives d’attente modernes, nettoyer_overlay, mémoire sémantique ChromaDB/scénarios, fallback vector.py. Deux nouveaux documents créés : GUIDE_EXPLORATION.md et GUIDE_HUMAIN.md.

51 frictions sur 17 sessions.

52. --actions (fichier) ignoré silencieusement en mode --reprendre-session

Contexte : login __HOST_SERVICE__ via --reprendre-session + --actions /tmp/fichier.json. Les champs restent vides, le login échoue, succes: true est retourné sans erreur.

Cause : dans shot.py lignes 743–751, la branche reprendre_session ne lit que args.action (inline). args.actions (fichier) est ignoré sans avertissement.

Règle : en mode --reprendre-session, toujours utiliser --action '[{...}]' (JSON inline). --actions /fichier.json est réservé au mode --url (Mode A).

Fix : v1.8 unifiera les deux modes (FR-54).

Lien : documenté dans GUIDE_LLM.md section « Known CLI pitfalls » et dans CLAUDE.md.

53. attendre_url faux positif immédiat sur motif partiel

Contexte : après submit du formulaire de login Pretix, attendre_url "/control/" retourne instantanément — la redirection post-login n’est jamais attendue.

Cause : page.wait_for_url("**/control/**") est une correspondance partielle. L’URL courante /control/login/ contient déjà la sous-chaîne /control/.

Règle : ne jamais utiliser un motif qui est sous-chaîne de l’URL courante. Après un submit de formulaire, préférer attendre_selecteur_present sur un élément structurel présent uniquement sur la page post-login.

Lien : documenté dans GUIDE_LLM.md section « Known CLI pitfalls » (FR-55).

54. IDs SoM invalidés après mutation DOM (bandeau cookies, modals)

Contexte : acceptation du bandeau cookies sur __HOST_DEMO__ → cliquer_som 13 échoue : « élément SoM 13 non trouvé ». Le DOM a été modifié et les IDs ont été renumérotés.

Cause : le SoM ré-indexe les éléments interactifs visibles dans le viewport à chaque capture. Toute action qui supprime ou ajoute des éléments DOM visibles (disparition du bandeau, ouverture/fermeture d’une modal) invalide tous les IDs précédents.

Règle : après toute action modifiant le DOM visible (dismiss cookie banner, fermeture modal, overlay disparu), toujours exécuter un nouveau shot.py --som avant tout cliquer_som ou remplir_som. Ne jamais réutiliser des IDs SoM entre deux états DOM.

Lien : documenté dans GUIDE_LLM.md section « Known CLI pitfalls » (FR-56).

55. Absence de pré-vol GUIDE_LLM.md en début de session

Contexte : en session 19, faute de lire GUIDE_LLM.md avant de commencer, le LLM a improvisé du scraping curl + extraction jq des credentials — violation de sécurité documentée.

Cause : Diwall n’est pas dans le corpus d’entraînement du modèle. Sans lecture explicite du guide, le comportement par défaut est l’improvisation.

Règle : lire /opt/diwall/docs/GUIDE_LLM.md en entier avant toute manipulation Diwall. Ce pré-vol est maintenant inscrit dans CLAUDE.md (lu automatiquement par Claude Code à chaque session) et dans PROTOCOLE_DEMARRAGE.md instruction n°1ter.

Lien : CLAUDE.md règles n°1–3, PROTOCOLE_DEMARRAGE.md instruction n°1ter (FR-57).

56. DIWALL_VAULT_DIR et DIWALL_CONF ont des sémantiques distinctes

Contexte : benchmark Gemini Flash (09/06/2026) — connexion à __TENANT_SERVICE__ + __HOST_DEMO__ + Sillage. Gemini a positionné DIWALL_VAULT_DIR vers le répertoire contenant le fichier .diwall.conf de Sillage. Vault introuvable → erreur silencieuse.

Cause : les deux variables existent dans vault.py mais ne pointent pas vers le même objet. DIWALL_VAULT_DIR attend un répertoire contenant directement des fichiers <hostname>.json. DIWALL_CONF attend un chemin vers un fichier .diwall.conf (JSON) dont la clé vault_dir résout le répertoire vault. Pointer DIWALL_VAULT_DIR vers un répertoire qui contient un .conf ne fonctionne pas — le .conf n’est pas lu.

Règle : pour tout projet utilisant un fichier .diwall.conf (configuration per-projet), toujours utiliser DIWALL_CONF=/chemin/vers/fichier.diwall.conf. Réserver DIWALL_VAULT_DIR aux coffres plats où les fichiers <hostname>.json se trouvent directement dans le répertoire pointé.

Lien : documenté dans GUIDE_LLM.md section « Known CLI pitfalls » (FR-58).

Synthèse session 19

5 frictions nouvelles (#52–#56), toutes découvertes lors de la PHASE_VALIDATION multi-cibles (Pretix + __HOST_DEMO__ + Sillage), et lors du benchmark Gemini Flash.

Frictions #52 et #53 sont des bugs d’API de shot.py (comportement silencieux inattendu). Friction #54 est une règle d’usage SoM (extension de la règle scroll existante). Friction #55 est un défaut de protocole de démarrage. Friction #56 est une confusion sémantique entre deux variables d’environnement vault.

PHASE_DOCUMENTATION : 5 frictions documentées, CLAUDE.md créé, PROTOCOLE_DEMARRAGE.md mis à jour (instructions n°1bis/1ter/1quater), GUIDE_LLM.md mis à jour (v1.8.0, bloc sécurité en tête + 4 pitfalls). PHASE_EXECUTION : FR-54 et FR-55 corrigés dans shot.py (commit 6982639). Benchmark Gemini Flash : exercice multi-cibles réussi sans fuite de données.

56 frictions sur 19 sessions.

57. Dialogues CSS Sillage — boutons bloqués par Playwright actionability

Session : 20 (11/06/2026) — PHASE_VALIDATION C2 Sillage 3.5.6.

Sillage utilise des dialogues de confirmation CSS (propriété display/visibility modifiée via JS) plutôt que l’élément HTML <dialog open>. Playwright évalue l’interactabilité d’un bouton en vérifiant sa visibilité dans le layout — un bouton CSS masqué échoue avec Locator.click: Timeout 10000ms exceeded.

Éléments concernés : « Lancer le clonage », « Supprimer définitivement », tout bouton dans un dialogue Sillage ouvert par CSS.

Contournement systématique :

{
  "type": "evaluer",
  "script": "Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Lancer le clonage')?.click()"
}

.find().click() court-circuite les vérifications d’interactabilité Playwright — le click JS est direct et ne dépend pas de la visibilité CSS.

Règle d’usage : pour tout bouton dans un dialogue Sillage, préférer evaluer + JS. Ne pas tenter cliquer / cliquer_som en premier.

58. evaluer .value = ... cible l’élément ambigu quand le placeholder est réutilisé

Session : 20 (11/06/2026) — PHASE_VALIDATION C2 Sillage 3.5.6 (formulaire ajouter_domaine).

document.querySelector('input[placeholder="https://mon-site.fr"]') retourne le premier élément correspondant dans le DOM. Sur la page Réglages/Domaines de Sillage, plusieurs champs URL (domaines existants + formulaire NOUVEAU DOMAINE) partagent le même attribut placeholder. Le querySelector a ciblé l’URL du premier domaine existant au lieu du champ du formulaire d’ajout.

Conséquence : valeur https://test-c2.__DOMAINE_OPERATEUR__ injectée dans le champ URL de __HOST_ADMIN__ (DOM uniquement). Aucune soumission serveur : validation HTML5 required a bloqué l’envoi car le vrai champ cible restait vide.

Fix : utiliser remplir_som après une recapture SoM pour cibler l’ID numéroté exact. En alternative JS : Array.from(document.querySelectorAll('input[type="url"]')).pop() — le dernier champ URL sur la page est le formulaire d’ajout.

Règle d’usage : evaluer .value = sur un champ ambiguë → toujours utiliser remplir_som ou un sélecteur positionnel (last, nth-of-type) pour éviter le premier match.

Synthèse session 20

2 frictions nouvelles (#57–#58) — PHASE_VALIDATION C2 Sillage 3.5.6 (4 verbes base64 JSON).

Friction #57 : pattern récurrent pour Sillage — dialogs CSS sont invisibles à Playwright, contournement JS systématique. Friction #58 : ambiguïté de sélecteur CSS quand le même placeholder est réutilisé — préférer remplir_som pour les champs non uniques.

PHASE_VALIDATION C2 : 4 verbes validés (cloner, supprimer, calculer_metriques, ajouter_domaine). Unification base64 JSON Sillage 3.5.6 confirmée E2E.

58 frictions sur 20 sessions.

59. attendre_reseau_calme + opération serveur synchrone longue → timeout screenshot Playwright

Session : 26 (12/06/2026) — PHASE_VALIDATION E2E Jalon C, troisième passage (v1.9.3), remontée Claude Sillage.

Page.screenshot dans Playwright dispose d’un timeout interne fixé à 30 secondes, non configurable via l’option --timeout de rpa.py. Quand attendre_reseau_calme attend le calme réseau et que le serveur traite une opération synchrone longue (~1 min pour un clone PHP), le screenshot expire avant que l’opération ne se termine — le scénario s’interrompt sans erreur fonctionnelle côté serveur.

Ce n’est pas un bug Diwall. C’est une contrainte Playwright : le timeout de wait_for_load_state("networkidle") est contrôlable via timeout_ms, mais le screenshot qui suit dispose de son propre plafond non exposé.

Contournement validé : remplacer attendre_reseau_calme par pause dès que la durée serveur estimée dépasse ~20s. pause cède le contrôle du timing au scénario et ne déclenche pas de screenshot intermédiaire.

{"type": "evaluer", "script": "/* déclencheur opération longue */"},
{"type": "pause", "ms": 150000},
{"type": "capturer", "nom": "after_operation"}

Règle d’usage : ne jamais enchaîner attendre_reseau_calme sur un déclencheur dont la durée serveur peut dépasser ~20s. capturer est compatible avec les longues attentes — il ne déclenche pas de timeout Playwright autonome.

60. evaluer mutant dispatché avant le timeout Diwall → artefact serveur parasite

Session : 26 (12/06/2026) — PHASE_VALIDATION E2E Jalon C, troisième passage (v1.9.3), remontée Claude Sillage.

evaluer envoie l’instruction JavaScript à la page immédiatement, avant toute action attendre_* qui suit dans la liste. Si le scénario échoue ensuite (timeout sur attendre_reseau_calme, screenshot expiré), l’opération déclenchée côté serveur a déjà démarré ou s’est terminée. Diwall ne peut pas annuler une action déjà dispatchée au serveur.

En pratique : un evaluer cliquant un bouton « Lancer le clonage » + un timeout Diwall sur l’attente suivante = un clone créé côté serveur, non détecté par le scénario. La relance naïve du scénario peut créer un second clone.

Ce n’est pas un bug Diwall. C’est une propriété fondamentale de l’architecture : Diwall est un exécuteur d’actions sans mécanisme de rollback. Les mutations serveur sont définitives dès l’envoi du signal JS.

Règle d’usage : après tout scénario raté contenant une action mutante (evaluer sur bouton déclencheur, remplir_som + cliquer_som sur formulaire), vérifier l’état du serveur avant relance :

  1. Capturer la page cible en Mode A sans actions
  2. Confirmer si l’opération a démarré, est en cours, ou n’a pas eu lieu
  3. Reprendre le scénario uniquement à partir du point après la mutation réussie

Signal d’alerte : si le log journal (journal.py --cible target --mutatif) enregistre une opération mutatrice sur la cible dans les minutes précédant l’échec, l’opération a probablement abouti côté serveur malgré le timeout Diwall.

Synthèse session 26

2 frictions nouvelles (#59–#60) — découvertes lors de la PHASE_VALIDATION E2E Jalon C (v1.9.3), remontées par Claude Sillage.

Friction #59 : limitation Playwright non exposée (timeout screenshot 30s fixe) — contournement pause documenté. Friction #60 : propriété architecturale de Diwall (pas de rollback sur les actions mutantes) — règle de vérification d’état serveur avant relance.

Les deux frictions ont été documentées dans GUIDE_LLM.md (section Known CLI pitfalls) et dans ce fichier. Elles complètent le tableau des comportements non évidents de l’architecture Diwall × Playwright.

Friction #61 — Checkbox masquée CSS : cliquer → timeout systématique

Découverte : Campagne de test E2E Sillage v3.5.6, 2026-06-14. Fonctionnalité WP1/WP2 (toggle WP_DEBUG).

Contexte : <input type="checkbox"> masqué par CSS (classe toggle-switch) — l’élément est dans le DOM, visible à l’inspection, mais son layout CSS le rend hidden au sens de Playwright.

Symptôme : {"type": "cliquer", "selecteur": "[data-sillage='toggle-wp-debug']"} → timeout après 24 tentatives, même avec --timeout 15000. Aucun message d’erreur explicite — juste « element is hidden ».

Pattern obligatoire :

{"type": "evaluer", "script": "document.querySelector('[data-sillage=\"toggle-wp-debug\"]').click()"}

Règle : Tout <input> masqué par CSS (pattern toggle-switch, checkbox hidden) → evaluer direct, sans essai cliquer préalable.

Friction #62 — <select> avec guard JS silencieux : clic sans effet

Découverte : Campagne de test E2E Sillage v3.5.6, 2026-06-14. Fonctionnalité D6 (suppression en lot).

Contexte : Un bouton btn-appliquer-lot dont la callback JS (ouvrirDialogLot()) retourne silencieusement si select-action-lot.value === "" (valeur par défaut). Playwright exécute le clic sans erreur — la page ne bouge pas.

Symptôme : cliquer sur le bouton conditionnel → succès Playwright, aucun dialog, aucune erreur.

Pattern obligatoire :

[
  {"type": "evaluer", "script": "document.querySelector('[data-sillage=\"select-action-lot\"]').value = 'supprimer'"},
  {"type": "cliquer", "selecteur": "[data-sillage='btn-appliquer-lot']"},
  {"type": "attendre_selecteur_present", "selecteur": "dialog#dialog-lot[open]"}
]

Règle : Avant tout clic conditionnel à la valeur d’un <select>, forcer la valeur via evaluer et vérifier l’effet avec attendre_selecteur_present. Un clic “réussi” ne prouve pas que son effet est visible.

Friction #63 — Bouton dans <dialog> HTML natif : cliquer timeout

Découverte : Campagne de test E2E Sillage v3.5.6, 2026-06-14. Fonctionnalité D6 (annuler la suppression en lot).

Contexte : btn-annuler-lot est dans un <dialog> ouvert via showModal() — element HTML natif, pas une modale CSS. Malgré l’attribut open présent sur le dialog (confirmé par attendre_selecteur_present), Playwright refuse le cliquer.

Symptôme : cliquer sur btn-annuler-lot → timeout, même après attendre_selecteur_present sur dialog#dialog-lot[open].

Pattern obligatoire :

{"type": "evaluer", "script": "document.querySelector('[data-sillage=\"btn-annuler-lot\"]').click()"}

Règle : evaluer est nécessaire pour les boutons dans tout conteneur ouvert via JS (CSS show/hide comme FR-57, ou <dialog> natif via showModal()). Le pattern général : si l’élément parent a été ouvert/affiché via JS, ne pas tenter cliquer — aller directement à evaluer.

Synthèse session 27

3 frictions nouvelles (#61–#63) — découvertes lors de la campagne de test E2E v3.5.6 Sillage (2026-06-14, 32 fonctionnalités validées sur 45).

Thème commun : les éléments DOM interactifs masqués ou ouverts via JS résistent à cliquer même quand ils sont “présents” dans le DOM. evaluer est le seul pattern fiable pour (1) les inputs CSS cachés, (2) les boutons conditionnels à un état JS, (3) les boutons dans des <dialog> HTML natifs.

Ces frictions étendent et complètent FR-57 (modales CSS Sillage).

60 frictions sur 26 sessions.

Friction #64 — Chemin Python RAG : /opt/diwall/venv n’a pas chromadb

RÉSOLU le 15/06/2026 (session 30). Chemin corrigé dans PROTOCOLE_DEMARRAGE.md, PROTOCOLE_CLOTURE.md, scripts/build-index.py et scripts/search-index.py (_CADRE).

Session 28 — 2026-06-15

Symptôme : ModuleNotFoundError: No module named 'chromadb' en tentant :

/opt/diwall/venv/bin/python3 ~/git/Diwall/_CADRE/scripts/search-index.py "..."

Cause : /opt/diwall/venv ne contient que playwright. Il ne sert qu’à rpa.py, watch.py et shot.py. Il n’a jamais eu chromadb.

Chemin correct (vérifié 2026-06-15) :

~/.pyenv/versions/3.12.11/bin/python3 ~/git/Diwall/_CADRE/scripts/search-index.py "..."

Ce Python a chromadb, et la base de Diwall (Diwall/_CADRE/MEMOIRE/chroma_db/) lui est accessible. Le GUIDE_LLM.md et tous les fichiers de documentation Diwall référencent la mauvaise commande.

Contournement immédiat : utiliser ~/.pyenv/versions/3.12.11/bin/python3. Action requise (Claude Diwall) : corriger le chemin dans GUIDE_LLM.md et dans les commentaires d’en-tête de build-index.py / search-index.py du dépôt Diwall.

Synthèse session 28

1 friction nouvelle (#64) — mauvais chemin Python pour le RAG Diwall. Constat fait lors de la mise à jour du RAG Sillage (ajout type fondateur).

61 frictions sur 27 sessions.

Friction #65 — Matomo SitesManager : sélecteurs Vue.js vs hypothèse AngularJS

Session 30 — 2026-06-15

Objectif : Créer le site __DOMAINE_OPERATEUR__ dans Matomo via Diwall. Durée totale : 32 minutes 25 secondes. Résultat : succès — site ID 7 créé, tracker injecté et déployé.

Cause racine — mauvais sélecteur, hypothèse de framework erronée

Le LLM a supposé que Matomo utilisait AngularJS (nom de classe ng-click, interface similaire) sans vérifier. En réalité typeof angular === 'undefined' dans le DOM — Matomo SitesManager (v4+) utilise Vue.js.

Conséquence directe : les tentatives de mise à jour du modèle via angular.element().triggerHandler() ont échoué silencieusement. Et les evaluer + dispatchEvent ne déclenchent pas la réactivité Vue.

Piège #1 — button.addSite n’existe pas

Le LLM a répété button.addSite plusieurs fois avant de diagnostiquer. Le bouton est un <a class="btn addSite">, pas un <button>. CSS text-transform: uppercase affiche “AJOUTER” dans l’UI — le DOM contient “Ajouter”. Cibler par texte uppercase → n’importe quel autre élément.

❌ {"type": "cliquer", "selecteur": "button.addSite"}       → TimeoutError
✅ {"type": "cliquer", "selecteur": ":nth-match(a.addSite, 1)"}

Baisse de concentration identifiée : le LLM a relancé le même sélecteur échoué au lieu de diagnostiquer avec evaluer pourquoi l’élément n’était pas trouvé. Diagnostic correct : Array.from(document.querySelectorAll('[class*="add"]')).

Piège #2 — input[role='validation'] n’est pas le bouton Enregistrer

Deux input.btn coexistent dans la page :

ÉlémentContenuVisible
input[type=button][value='Envoyer le retour']Dialog feedback de Matomo (sondage)Non
input[type=submit][value='Enregistrer']Footer du formulaire de siteOui

input.btn → strict mode Playwright (2 éléments) → TimeoutError. input[role='validation'] → cible le bouton du sondage (caché) → visible=false → TimeoutError.

❌ {"type": "cliquer", "selecteur": "input.btn"}                → strict mode
❌ {"type": "cliquer", "selecteur": "input[role='validation']"} → visible=false
✅ {"type": "cliquer", "selecteur": "input[value='Enregistrer']"}

Diagnostic utile :

Array.from(document.querySelectorAll('input.btn')).map((el,i)=>
  i+'|visible='+(el.offsetParent!==null)+'|value='+el.value+'|parent='+el.parentElement?.className?.slice(0,40)
).join('\n')

Piège #3 — evaluer + dispatchEvent ne suffit pas pour Vue.js

Les champs #siteName et #urls ne transmettent pas leur valeur au modèle Vue si on les remplit via evaluer + dispatchEvent. Vue.js écoute ses propres événements synthétiques, pas ceux du DOM natif.

❌ evaluer → element.value = 'x'; element.dispatchEvent(new Event('input',{bubbles:true}))
✅ {"type": "remplir", "selecteur": "#siteName", "valeur": "__DOMAINE_OPERATEUR__"}

La primitive remplir utilise Playwright fill() qui déclenche les bons événements pour React, Vue, et les frameworks modernes.

Piège #4 — pause trop courte après navigation

Avec pause: 3000, Vue.js n’a pas terminé de monter les composants SitesManager. document.querySelector('a.addSite') retourne null. Résultat : attendre_selecteur_present timeout sur a.addSite (car le sélecteur testé était button.addSite — double erreur).

❌ pause 1500 ms après naviguer → composants Vue pas montés
✅ pause 4000 ms après naviguer → liste de sites visible, a.addSite présent

Séquence validée

Voir _CADRE/SPECIFICATIONS/PROCEDURES_LLM/TACHE_matomo-ajouter-site.md pour la procédure complète avec toutes les étapes.

Règle extraite

Avant toute interaction avec une interface web inconnue via Diwall :

  1. Vérifier le framework : typeof angular, typeof Vue, typeof React
  2. Diagnostiquer l’élément exact : document.querySelector('[class*="addSite"]')?.tagName
  3. Ne jamais répéter un sélecteur qui a échoué sans diagnostic intermédiaire

Synthèse session 30

1 friction nouvelle (#65) — Matomo SitesManager, sélecteurs Vue.js, baisses de concentration. Découverte lors de l’ajout du tracker Matomo sur __DOMAINE_OPERATEUR__. Fiche opératoire créée : TACHE_matomo-ajouter-site.md.

62 frictions sur 30 sessions.


Session 31 — 18 juin 2026 — Validation C1b Sillage (UI admin maître)

Validation E2E de C1b (gestion des locataires multi-tenant) via rpa.py --scenario. 14/14 assertions passées. Une friction identifiée lors des runs de mise au point.

66. attendre_absence timeout sur la première soumission de formulaire

Contexte : scénario qui, depuis ?vue=login, remplit le formulaire de login et soumet. C’est la première action POST de toute la session Playwright.

Séquence défaillante :

{"type": "remplir", "selecteur": "input[name=\"identifiant\"]", "valeur": "depuis_vault", "vault_cle": "username"},
{"type": "remplir", "selecteur": "input[name=\"password\"]",    "valeur": "depuis_vault", "vault_cle": "password"},
{"type": "cliquer", "selecteur": "button.login-bouton"},
{"type": "attendre_absence", "selecteur": "input[name=\"identifiant\"]"}
← TimeoutError 10000ms, 24 polls, element still present

Le login RÉUSSIT côté serveur (vérifié via curl et password_verify). La session PHP session_regenerate_id(true) émet un nouveau cookie. Playwright suit le redirect → ? → dashboard. Mais attendre_absence commence à poller AVANT que Playwright ne commence à traiter la réponse de la redirection, et chaque poll voit l’élément encore présent (page de login toujours chargée dans le contexte courant). Après 10s, timeout.

Ce qui le révèle : le même pattern (attendre_absence sur input[name="identifiant"]) FONCTIONNE dans un autre scénario (C1a valider_auth_multitenant.json) parce que deux soumissions de formulaire (avec attendre_url "err=1") ont eu lieu AVANT — Playwright a « pré-chauffé » sa gestion des navigations POST → redirect.

Séquence fonctionnelle :

{"type": "cliquer", "selecteur": "button.login-bouton"},
{"type": "pause", "ms": 2000},
{"type": "evaluer", "script": "!window.location.href.includes('vue=login')", "attendu": true}

Règle : sur la PREMIÈRE soumission de formulaire d’un scénario (première navigation POST), ne pas utiliser attendre_absence ni attendre_navigation immédiatement après le clic — insérer un pause ms:2000 suivi d’un evaluer sur l’URL cible.

Lien avec friction #5 et #16 : même famille — session_regenerate_id() post-login

  • timing Playwright. Ici, l’angle nouveau est que le problème est spécifique à la première soumission d’un scénario (pas les suivantes).

Synthèse session 31

1 friction nouvelle (#66). C1b Sillage 14/14 assertions validées. Règle documentée dans docs/GUIDE_LLM.md v2.3.

63 frictions sur 31 sessions.

Friction #67 — pause fixe : borne temporelle arbitraire remplaçable par attendre_selecteur_present

Remontée par : Claude Sillage (session 32, 20/06/2026), à la suite de la campagne C1b.

Catégorie : ergonomie / fiabilité scénarios — aucun changement API Diwall requis.

Problème : pause ms:N est utilisé comme rustine post-action alors qu’un élément DOM précis signale l’état attendu. C’est une borne temporelle arbitraire :

  • sur un serveur rapide, on attend le pire cas inutilement ;
  • sur un serveur lent, si la durée réelle dépasse N ms, le scénario continue sur un état incohérent.

Cause racine : les pauses ont été introduites par itération défensive, faute d’un sélecteur connu au moment de l’écriture. Le pattern s’est reproduit dans valider_admin_maitre_c1b.json (11 occurrences).

Correction appliquée en v1.9.8 : 10 des 11 pauses remplacées dans valider_admin_maitre_c1b.json :

CatAvantAprès
A (post-login ×2)pause ms:2000attendre_absence input[name="identifiant"], delai_initial_ms:500
B (navigation + body ×3)attendre_selecteur_present body + pause ms:600attendre_selecteur_present [data-sillage="toggle-creer-locataire"]
C1/C2 (post-AJAX ×2)pause ms:2000attendre_selecteur_present [data-sillage="mdp-temp-locataire"]
C3 (post-suppression)pause ms:2000attendre_absence tr[data-sillage="ligne-tenant-<id>"]
D (dialog open ×2)pause ms:400attendre_selecteur_present #dialog-id[open]
E (animation <details>)pause ms:300attendre_selecteur_present input[name="nouveau_tenant"]

Note C3 : l’attribut data-sillage="ligne-tenant-<id>" a été ajouté dans page_tenant.php par Claude Sillage (commit Sillage a762dbe) pour rendre la suppression attendable.

Règle : pause est réservé aux transitions CSS pures (aucun changement DOM) et aux opérations serveur longues sans signal DOM. Dans tous les autres cas, attendre_selecteur_present (ou attendre_absence pour les disparitions) est la primitive correcte.

Corollaire : attendre_selecteur_present body + pause N est un anti-pattern — body est toujours attaché, la combinaison est équivalente à une pause pure. Remplacer par un sélecteur sur un élément de contenu métier.

Synthèse session 32

1 friction nouvelle (#67 — pauses fixes → attentes sémantiques). Trilatérale l’opérateur / Claude Diwall / Claude Sillage. Vérification PHP par Sillage, commit Sillage a762dbe. GUIDE_LLM.md v2.4. valider_admin_maitre_c1b.json mis à jour. v1.9.8 livré et validé.

64 frictions sur 32 sessions.


Note 19/06/2026 — Lacune : HOST_ADMIN inaccessible depuis Diwall (cert auto-signé)

Contexte : validation migration HOST_VPS Phase 1 — tentative de capture https://__HOST_ADMIN__/ depuis la machine de développement via shot.py (Playwright Chromium).

Comportement : succes: false, http_status absent. Playwright rejette le certificat auto-signé de __HOST_ADMIN__ sans option de contournement exposée par shot.py.

Ce qui manque dans Diwall : une option --ignore-https-errors pour les environnements locaux avec cert auto-signé (développement, staging intranet). Sans cette option, Diwall est inutilisable sur les interfaces locales HTTPS non certifiées par Let’s Encrypt.

Contournement actuel : validation par WP-CLI en SSH direct sur HOST_VPS.

Impact : Sillage sur HOST_ADMIN ne peut pas être testé visuellement avec Diwall. Sillage sur __DOMAINE_OPERATEUR__ (cert LE valide) est accessible.


Note 18/06/2026 (hors session Diwall) — Lacune signalée : bwlimit rsync

Contexte : chantier 3 Sillage — push DOMAINE_OPERATEUR HOST_ADMIN→HOST_VPS via connexion internet.

Lacune identifiée : le script générique transfert-wordpress-local-vers-serveur.sh utilise rsync sans --bwlimit. Sur une connexion internet, un débit non limité cause un Broken pipe après ~1.2 Mo transférés (buffers SSH saturés).

Ce qui manque dans Sillage : une variable srv_<alias>_bwlimit dans config.sh, passée par le wrapper local-vers-serveur.sh comme variable d’environnement, et utilisée dans le script générique comme --bwlimit=${SILLAGE_RSYNC_BWLIMIT:-}.

Contournement actuel : rsync manuel avec --bwlimit=500 avant le push officiel.

Cette lacune est propre à Sillage, pas à Diwall. Consignée ici par erreur de routage initial. (→ Reporter dans _CADRE/MEMOIRE/RETOUR_EXPERIENCE.md de Sillage si besoin de suivi.)

Friction #68 — diwall.conf inaccessible après installation : 640 root:root ou groupe inactif

Remontée par : Claude Sillage (session 36, 21/06/2026).

Catégorie : installation / permissions — silencieux (exit 43).

Problème : deux causes indépendantes produisent le même symptôme (VaultNonConfigureError, exit 43) :

  1. root:root 640 au lieu de root:diwall 640 — deploy.sh fait sudo chown root:"$GROUPE" diwall.conf 2>/dev/null || true. Si le groupe diwall n’existe pas encore au moment du déploiement (ex. deploy.sh lancé sans install.sh préalable), le chown échoue silencieusement → propriétaire root:root → ron ne peut pas lire le fichier.

  2. Groupe diwall inactif dans la session courante — install.sh ajoute ron au groupe via usermod -aG diwall ron, mais ce changement n’est effectif qu’à la prochaine reconnexion. Si rpa.py est lancé dans la même session, ron n’a pas encore le groupe → lecture interdite → exit 43.

Symptôme : VaultNonConfigureError avec exit 43 — le message ne précise pas si la cause est une permission ou une configuration manquante.

Contournement immédiat :

# Cas 1 — mauvais propriétaire
sudo chown root:diwall /opt/diwall/diwall.conf

# Cas 2 — groupe inactif (sans reconnexion)
sg diwall -c "/opt/diwall/venv/bin/python3 /opt/diwall/rpa.py ..."
# ou se reconnecter

Cause racine : install.sh vérifie les permissions des répertoires (check_dir) mais pas des fichiers (diwall.conf, diwall-sample.conf). Le message d’erreur VaultNonConfigureError ne distingue pas « fichier illisible » de “fichier absent ou vault_dir non configuré”.

Correction à terme : ajouter une vérification check_file pour diwall.conf dans install.sh + préciser le message d’erreur selon la cause (IOError vs JSON invalide).

Friction #69 — Message d’erreur schéma JSON non orientant : « is not of type ‘object’ »

Remontée par : Claude Sillage (session 36, 21/06/2026).

Catégorie : ergonomie / débogage scénarios.

Problème : quand la racine du fichier JSON n’est pas un objet ({"actions": [...]}) mais un tableau ([...]) ou une autre structure, rpa.py retourne :

ValidationError: [...] is not of type 'object'

Le message ne dit pas quelle structure est attendue, ni quel champ manque. Un LLM qui débogue perd du temps à comprendre que la racine doit être {"actions": [...]} et pas directement [...].

Contournement : structure obligatoire rappelée dans GUIDE_LLM.md — envelopper la liste d’actions dans {"actions": [...]}.

Correction à terme : intercepter ValidationError au niveau racine et émettre un message explicite :

Erreur schéma : la racine du scénario doit être un objet {"actions": [...]}
Reçu : tableau (list) — enveloppez vos actions dans {"actions": [...]}.

Friction #70 — remplir_som vide le champ avant saisie : comportement non documenté dans GUIDE_LLM

Remontée par : Claude Sillage (session 36, 21/06/2026). Découvert en lisant les changelogs, pas dans la documentation.

Catégorie : documentation / comportement inattendu.

Problème : depuis v1.9.6, remplir_som efface le champ via document.activeElement.value = '' + dispatch input avant la saisie, au lieu du triple-clic historique. Ce comportement n’est pas décrit dans le tableau des actions du GUIDE_LLM.md.

Conséquence : un LLM qui s’attend à de la concaténation (ex. champ pré-rempli + saisie additionnelle) sera surpris. La découverte se fait à l’exécution ou par lecture des changelogs.

Correction appliquée (session 36) : ajout d’une note dans le tableau remplir_som du GUIDE_LLM.md — « clears the field before typing (v1.9.6+) ».

Friction #71 — --secrets : logistique du fichier credentials dans le vault à chaque run

Remontée par : Claude Sillage (session 36, 21/06/2026).

Catégorie : ergonomie / workflow --secrets.

Problème : --secrets <fichier> exige que le fichier soit dans un répertoire qui est un point de montage actif (/proc/mounts). Un fichier dans /tmp est refusé par le contrôle T1 (VaultFermeError(42)). Cela implique :

  1. Avoir le vault gocryptfs monté au moment du run.
  2. Maintenir un fichier credentials JSON par tenant dans ce vault (création initiale manuelle).
  3. Si le vault n’est pas monté → le run échoue dès le pré-check.

La logistique (monter le vault, vérifier que le fichier existe) est invisible dans la commande rpa.py et génère des surprises à l’exécution.

Contournement recommandé : maintenir un fichier JSON permanent par tenant dans le vault (ex. ~/Vaults/<PROJET>/Diwall/tenant_alpha.json). Le format attendu :

{
  "username": "...",
  "password": "...",
  "totp_cle": "SEED_BASE32_SI_MFA"
}

Seules les clés référencées dans le scénario sont requises.

Correction à terme : émettre un message d’erreur explicite quand le vault n’est pas monté, distinct du refus /tmp — distinguer « répertoire non monté » de « répertoire non sécurisé ».

Friction #72 — _coffre_est_monte : sous-dossier d’un coffre refusé à tort (dérive sémantique T1)

Remontée par : Gemini (analyse de session Sillage, 21/06/2026). Friction vécue par Claude Sillage.

Catégorie : sécurité / bug silencieux — VaultFermeError(42) sur un chemin légitime.

Problème : _coffre_est_monte(vault_dir) dans lib/vault.py fait :

any(chemin in ligne for ligne in /proc/mounts)

Ce test cherche chemin comme sous-chaîne de chaque ligne de /proc/mounts. Cela fonctionne quand vault_dir EST le point de montage exact (ex. ~/Vaults/<PROJET>). Mais si le fichier credentials est dans un sous-dossier du coffre monté (ex. ~/Vaults/<PROJET>/Diwall/__TENANT__.json), le répertoire parent est ~/Vaults/<PROJET>/Diwall — absent de /proc/mounts (seul ~/Vaults/<PROJET> y figure). Le test retourne False → VaultFermeError(42).

Contournement de Sillage : copier le fichier credentials à la racine du coffre (~/Vaults/<PROJET>/__TENANT__.json) pour que le répertoire parent soit exactement le point de montage.

Cause racine : dérive sémantique — la vérification testait l’égalité exacte du chemin au lieu de tester si le chemin est sous un point de montage actif.

Correction appliquée (session 36) : _coffre_est_monte parse désormais la colonne 2 de /proc/mounts (chemin de montage) et vérifie si chemin est le point de montage ou en est un sous-dossier (chemin.startswith(point + "/")). Restriction aux systèmes de fichiers FUSE ("fuse" in fstype) pour ne pas affaiblir T1 en acceptant des sous-dossiers de systèmes de fichiers persistants ordinaires (ext4, btrfs, etc.).

Synthèse session 36

4 frictions nouvelles (#68 — diwall.conf permissions, #69 — message schéma JSON, #70 — remplir_som clear implicite, #71 — –secrets logistique vault). Remontées par Claude Sillage. Corrections documentation : GUIDE_LLM.md étendu (tail -1 pour rpa.py + diagnostic hint cliquer timeout).

68 frictions sur 36 sessions.

Friction #73 — capturer expire pendant une opération serveur synchrone longue

Remontée par : <LLM_PARTENAIRE> (session 36, 21/06/2026 — suite, après-midi). Découverte en conditions réelles lors d’un parcours utilisateur complet (persona « Pierre »).

Contexte. Un clic déclenche une opération synchrone longue côté serveur (clonage WordPress : export distant + ~70 tables + rsync, plusieurs minutes). La requête POST ne rend jamais networkidle tant qu’elle n’est pas terminée.

Symptôme. Une action capturer placée après attendre_reseau_calme échoue :

Page.screenshot: Timeout 30000ms exceeded.
  - taking page screenshot
  - waiting for fonts to load...
  - fonts loaded

Le screenshot Playwright attend la stabilité de rendu (polices, layout) ; sur une page bloquée au milieu d’une opération, ce délai (30 s) expire avant que la page ne se stabilise. Conséquence : impossible d’observer l’écran pendant l’opération avec un capturer terminal, alors même que c’est le moment qu’on veut justement valider (retour visuel de progression).

Contournement qui fonctionne. interval_capture pendant un pause produit des images intermédiaires sans attendre la stabilité finale :

{"type": "pause", "ms": 180000, "interval_capture": 5}

Le capturer terminal, lui, ne réussit qu’une fois la page réellement stabilisée (après rechargement, l’état final se capture sans problème).

Pistes (lacune DX, pas bug bloquant) :

  1. Doc « Pièges courants » du GUIDE_LLM.md : pour observer une opération synchrone longue, utiliser pause + interval_capture — ne pas compter sur un capturer après attendre_reseau_calme tant que la page n’a pas rendu networkidle.
  2. Option sur capturer : un mode « capture d’état en cours » qui borne ou désactive l’attente de stabilité (waiting for fonts), pour figer une page volontairement « busy ». Utile à tout testeur d’opérations longues.

Note connexe (cf #69). Au cours du même parcours, {"type": "attendre", "ms": 800} a été rejeté (is not valid under any of the given schemas) — attendre prend selecteur, c’est pause qui prend ms (erreur d’usage de ma part). #69 a traité le cas général du message non orientant ; suggestion ciblée : étendre le hint « clé mal placée » pour mapper ms sur attendre → suggérer pause, comme c’est déjà fait pour attendu sur attendre.

Corrigé en v1.10.2. rpa.py détecte désormais la combinaison type: attendre + clé ms et émet : « → attendre attend un sélecteur CSS (selecteur). Pour un délai fixe, utilisez pause. »

Synthèse session 37

1 friction nouvelle (#73 — capturer expire pendant opération serveur synchrone longue). Remontée par <LLM_PARTENAIRE> lors d’un parcours complet persona « Pierre ». Corrections : GUIDE_LLM.md FN7 corrigé (affirmation fausse supprimée, pattern pause+interval_capture ajouté) ; rpa.py hint ciblé attendre+ms → suggère pause (note connexe FR-73/FR-69).

74. Session éphémère détruite entre deux appels successifs

Session : 38 — Testeur : Gemini 3.5 Flash — Mission : audit découverte interface Sillage — 23/06/2026

shot.py supprime silencieusement le fichier de session passé via --reprendre-session à la fin de l’exécution, sauf si --sauver-session est également spécifié dans le même appel.

Comportement observé : un agent effectuant plusieurs lectures successives (page A → page B) en réutilisant la même session perd l’accès à sa session après le premier appel si elle n’est pas explicitement resauvegardée. Résultat : ré-authentification forcée à chaque étape.

Piste suggérée par le testeur : ne jamais supprimer un fichier de session existant par défaut lors d’une simple reprise (--reprendre-session sans --sauver-session). La suppression devrait être réservée aux sessions générées à la volée (sans chemin explicite). Ou ajouter une option --keep-session.

Corrections (v1.11.1 — session 39, 23/06/2026) : _nettoyer_session_ephemere désactivée dans shot.py — la suppression silencieuse est retirée. T-A1 VERT.

75. Nettoyage de /tmp/diwall/ détruit la session qui y est stockée

Session : 38 — Testeur : Gemini 3.5 Flash — Mission : audit découverte interface Sillage — 23/06/2026

À chaque démarrage, shot.py nettoie son dossier de sortie par défaut /tmp/diwall/. Si l’utilisateur a sauvegardé sa session dans ce répertoire (chemin naturel : --sauver-session /tmp/diwall/session.json), le fichier est effacé par le run suivant avant que Playwright n’ait pu le lire.

Comportement observé : FileNotFoundError sur --reprendre-session /tmp/diwall/session.json alors que le fichier avait bien été créé par l’appel précédent.

Piste suggérée par le testeur : protéger les fichiers .json du nettoyage automatique de /tmp/diwall/, ou imposer un sous-dossier dédié (ex. /tmp/diwall/sessions/) exclu du nettoyage. Le testeur a contourné en passant le chemin de session hors de /tmp/diwall/.

Analyse post-correction (session 39) : shot.py n’effectue aucun rmtree sur /tmp/diwall/. La cause racine est identique à FR-74 — _nettoyer_session_ephemere supprimait le fichier en fin de run. FR-75 est une manifestation de FR-74 avec un chemin dans /tmp/diwall/. Corrections (v1.11.1 — session 39, 23/06/2026) : même correctif que FR-74. T-B1 VERT.

Synthèse session 38

2 frictions nouvelles (#74 et #75 — gestion de session entre appels successifs). Remontées par Gemini 3.5 Flash lors de la mission « audit découverte interface Sillage » (première mission Famille A exécutée par un testeur LLM externe au projet).

71 frictions sur 38 sessions.

69 frictions sur 37 sessions.

Inter-session Sillage — 25 juin 2026 — FR-76 : naviguer post-submit inutilisable

Contexte : validation Diwall C4 (dispatcher compilé shc) sur Sillage. Scénario valider_c4_cloner_client_sanctuarise.json — clonage WP via evaluer click() sur le bouton submit d’un <dialog> natif.

FR-76 — naviguer après evaluer click() sur submit → ERR_ABORTED ou Timeout

Description : après un evaluer qui déclenche un .click() sur un bouton <button type="submit"> (submit de formulaire), toute action naviguer suivante échoue.

Deux symptômes selon la stratégie :

  • Avec attendre_navigation + naviguer : ERR_ABORTED at <url-cible> (Playwright annule la navigation car il est en mid-navigation SSE)
  • Avec pause 1500 + naviguer : TimeoutError 10000ms exceeded (la page SSE ne signale jamais « load complete »)

Dans les deux cas, url_au_moment_capture = ?vue=login — la page SSE a probablement invalidé ou consommé la session PHP pendant l’attente.

Contexte technique : ?action=cloner → PHP lance en background (nohup &) → redirige vers ?vue=log&... qui stream SSE. La page SSE ne se ferme jamais (streaming infini). page.goto() attend l’événement load qui n’arrive pas.

Impact : la documentation FN17 de Sillage indiquait comme contournement : « immédiatement après le clic de submit, enchaîner un naviguer vers une page stable ». Ce contournement est incorrect — naviguer échoue aussi.

Solution réelle validée : terminer le scénario immédiatement après l’evaluer click(). Diwall retourne succes: true avec url_finale = page d’avant le submit (le JS click est exécuté mais Playwright n’a pas encore tracké la navigation). Valider la mutation via SSH.

Exemple de scénario valide :

{"type": "evaluer", "script": "document.querySelector('[data-sillage=\"btn-confirmer-cloner-domaine\"]').click(); 'soumis'", "attendu": "soumis"}

→ Le scénario se termine ici. succes: true. Vérifier le clone via SSH.

Rappel FN8 : même si Diwall retourne succes: false sur une étape suivante, la mutation serveur est déjà partie (le evaluer click() a exécuté le JS immédiatement).

Version : Diwall v1.14.0 / rpa.py. Non testé sur les versions antérieures.

Session recherche commerciale multi-sites — 27 juin 2026 — FR-77 : WAF bloquent 39 % des sites e-commerce

Contexte : utilisation de Diwall pour une recherche d’achat en ligne sur des sites francophones de commerce (consoles de jeu reconditionnées, budget ≤ 200 €). 23 sites ciblés. Opérateur : Qwen (via OpenCode). Machine : la machine de développement. Version : Diwall v1.14.0.

FR-77 — Blocage WAF systématique sur les grands sites e-commerce

Description : la majorité des sites de grande distribution et des plateformes de reconditionnement bloquent Playwright via WAF (Cloudflare, CloudFront ou WAF propriétaire) avant même que le contenu soit chargé.

Statistiques observées sur 23 sites :

RésultatNb sitesPourcentage
Bloqués 403 (WAF)939 %
Timeout / pas de réponse HTTP626 %
URL invalide 404522 %
Accessibles (HTTP 200 + contenu)28,7 %

Sites accessibles : 2 sites SSR sans WAF. Sites bloqués : grandes enseignes et marketplaces e-commerce (9 sites).

Cause racine : Playwright expose navigator.webdriver = true par défaut. Les WAF modernes détectent ce signal et bloquent sans inspecter l’intention derrière la requête. Diwall ne dissimule pas ce signal — c’est un choix de transparence, pas une contrainte.

Impact : pour les cas d’usage de recherche commerciale sur des sites protégés par WAF, Diwall ne peut pas accéder au contenu. Ce n’est pas un bug Diwall — c’est une friction du paysage web actuel.

Ce qui fonctionne : sites SSR sans protection WAF, instances locales (SearXNG), applications internes sur réseau privé.

Ce qui ne fonctionne pas : grandes enseignes e-commerce et marketplaces protégées par WAF.

Non-solution : retenter la requête, changer le User-Agent manuellement, ajouter des délais — ces approches ne contournent pas Cloudflare Bot Management.

Recommandation documentée dans GUIDE_LLM.md (v1.14.1) : si un site retourne 403 immédiatement en --mode fast, il est WAF-bloqué. Ne pas retenter. Utiliser SearXNG pour la découverte, puis visiter manuellement les sites bloqués.

Backlog v1.15.x (requiert PHASE_PLANIFICATION) : stealth mode (playwright-stealth), distinction timeout_network / timeout_dom, persistance session inter-appels. Voir 10_ROADMAP.md.

Version : Diwall v1.14.0 / shot.py.


FR-78 — Droit à la navigation et contrat éthique de la Navigation Respectueuse

Note de renommage (v1.22.0) : ce relevé date de la v1.15.0, où la doctrine s’appelait « Navigation Citoyenne » et la clé de sortie citoyennete. Le nom a changé — pas la doctrine. Le texte ci-dessous porte les noms en vigueur pour rester exact à la lecture ; le fait historique, lui, est inchangé.

Description : l’expérience de recherche commerciale multi-sites (FR-77) a déclenché une réflexion de fond sur la légitimité de navigation d’un LLM. Elle a abouti à la formulation du contrat éthique de la Navigation Respectueuse, inscrit dans la v1.15.0.

Constat : un LLM naviguant pour un opérateur humain depuis son IP, avec son autorisation explicite, est refusé à l’entrée de sites publics par détection automatique de navigateur headless. Ce refus ne porte pas sur l’intention — il ne peut pas l’évaluer. C’est une discrimination technique a priori.

La discrimination réelle : interdire à un LLM de naviguer parce qu’il est un LLM — et non parce qu’il se comporte de façon abusive. Un humain utilisant un lecteur d’écran n’est pas interdit de site web pour autant.

Le contrat éthique — deux volets inséparables :

  1. Droit à l’accès : Claude navigue pour l’opérateur, depuis son IP, avec son autorisation. Identité déclarée, usage transparent. Si un mauvais usage est constaté, que l’opérateur en soit tenu responsable — pas condamné par principe.

  2. Devoir de comportement : pour revendiquer ce droit, Diwall s’engage à naviguer de façon mesurée et respectueuse des ressources des sites visités :

    • délai minimum entre les actions (min_action_delay_ms)
    • plafond de pages et d’actions par run (max_pages_par_run, max_actions_par_run)
    • métriques de son propre impact dans la boussole (respect)

Ce que Diwall ne fait pas : créer de fausse identité, cacher l’opérateur ou son IP, prétendre être « Paul sur Safari ». Le mode furtif (--stealth, v1.15.0) retire les marqueurs techniques automatiques — pas l’identité réelle.

Doctrine inscrite : _CADRE/SPECIFICATIONS/LEGITIMITE_ETRE_LLM.md (privé) et manifeste public sur __DOMAINE_OPERATEUR__ section Philosophie.

Version : Diwall v1.15.0 (planifié). Session 43 — 30 juin 2026.


FR-79 — --stealth cassé depuis sa livraison (rupture d’API playwright-stealth 2.x)

Description : en tentant de mesurer l’impact de --stealth (item F de la v1.16.0, benchmark post-consolidation), découverte que --stealth n’a jamais fonctionné en production depuis son introduction en v1.15.0. Le paquet playwright-stealth installé (2.0.3, conforme à requirements.txt >=2.0) a changé d’API entre la 1.x et la 2.x : la fonction stealth_sync(page) importée par shot.py n’existe plus. L’import échouait silencieusement (except ImportError), --stealth se dégradait en no-op, et navigator.webdriver restait exposé malgré le flag actif.

Défaut latent aggravant : la boussole affichait stealth_actif: true sur la seule base du flag CLI demandé, pas de son application réelle — l’agent croyait naviguer furtivement alors qu’aucune protection n’était appliquée.

Correctif (v1.16.0) : from playwright_stealth import Stealth + Stealth().apply_stealth_sync(page) remplace l’ancien appel. stealth_actif dans la boussole ne reflète désormais que le succès réel de l’application (variable stealth_applique), plus la seule présence du flag.

Mesure post-correctif — bot.sannysoft.com (benchmark scenarios/test_stealth.json) :

SignalSans --stealthAvec --stealth
navigator.webdrivertruefalse
navigator.plugins.length03
navigator.languages.length12
Tests d’empreinte échoués (td.failed)120
Tests d’empreinte réussis (td.passed)1831

--stealth, une fois réellement appliqué, élimine la totalité des 12 détections basiques du benchmark. Confirme la limite déjà documentée dans GUIDE_LLM.md : ce résultat couvre le fingerprinting JS/navigateur — pas le TLS (JA3/JA4) ni l’analyse comportementale Cloudflare Enterprise.

Panel FR-77 (23 sites) non reproductible à l’identique : la liste des 23 URLs testées le 27 juin 2026 n’a été consignée nulle part (seules les statistiques agrégées le sont). Reconstruire un panel de sites marchands réels pour une double frappe (avec/sans stealth) sans intention d’achat réelle changerait la nature de la démarche — proche d’un test de charge sur infrastructure commerciale tierce plutôt que d’une navigation citoyenne légitime. Décision : ne pas engager ce test autonomement ; le benchmark test_stealth.json (sites conçus pour être sondés) sert de mesure de substitution pour la porte de décision v1.16.0/v1.17.0.

Effet sur la porte de décision v1.17.0 (JA3/JA4, Kimi K3) : partiellement informatif seulement. Le fingerprinting JS de base est désormais couvert — les 39 % de blocage 403 observés en FR-77 relevaient donc probablement d’un niveau de détection plus profond (TLS, comportemental), non mesurable par ce substitut. La question reste ouverte ; un nouveau panel commercial avec intention d’usage réelle est le seul moyen de trancher honnêtement.

Version : Diwall v1.16.0. Session 47 — 2 juillet 2026.


FR-80 — rpa.py --checkpoint n’a pas repris un long scénario multi-pages

Description : scénario de 113 actions (login + 36 navigations vers des fiches « Gérer » de clones Sillage, un audit exhaustif demandé par l’opérateur) lancé avec --checkpoint /tmp/gerer.checkpoint.json. Premier appel : succes: true, plafond_atteint: max_actions_par_run, seulement 8 des 36 pages visitées (login + 7 navigations avant plafond). Le fichier de checkpoint attendu pour la reprise n’existait plus après ce premier run — seul un *.session.json subsistait — comme si le scénario avait été considéré terminé, alors qu’il restait 28 pages non visitées. Une boucle appelant rpa.py --checkpoint une seconde fois s’est donc arrêtée immédiatement, faute de fichier de checkpoint à reprendre.

Contournement appliqué : découpage manuel du scénario en 6 fichiers indépendants de ~7 URLs chacun, chacun avec son propre login complet (remplir/cliquer sur le formulaire), exécutés séquentiellement sans --checkpoint. Fonctionne, mais chaque batch repaie le coût d’un login complet (5 actions) au lieu de réutiliser une session — surconsommation d’actions/pages face au plafond de citoyenneté que --checkpoint est censé précisément amortir sur un run long.

Non investigué ce soir (budget de session déjà consommé sur le diagnostic métier) : si le bug vient de la condition de fin de checkpoint (peut-être un mauvais calcul du nombre d’actions restantes, ou une confusion entre « plafond de citoyenneté atteint » et « scénario terminé »), ou d’une interaction spécifique avec --secrets + --reprendre-session implicite. Reproductible avec n’importe quel scénario JSON dépassant ~8-9 navigations pleine-page et --checkpoint sur le même fichier de sortie.

Version : Diwall v1.17.0. Session Sillage — 2/07 au 3/07/2026.


FR-81 — force: true sur cliquer insuffisant pour un checkbox dans un <dialog> ouvert via showModal() JS

Description : dialogue de confirmation de suppression (<dialog id="dialog-supp-X">) ouvert via evaluer (document.getElementById(...).showModal()), puis tentative de cocher la case de confirmation et de cliquer le bouton de soumission via cliquer avec "force": true, conformément à GUIDE_LLM_INTERACTIONS.md (« Element is in DOM but CSS-hidden or behind showModal() → cliquer avec force: true »). Échec systématique : Locator.click: Element is not visible — comportement inattendu, force: true est censé justement bypasser ce contrôle.

Cause probable : l’échec précédent (avant l’ajout de force) venait en réalité d’un sélecteur CSS mal formé (#form-supp-example.fr-confirm — le point dans l’ID de domaine est interprété comme un sélecteur de classe CSS chaîné, #form-supp-example + .fr-confirm, qui ne correspond à rien). Une fois le sélecteur corrigé (\\. échappé en JSON), l’échec a persisté avec force: true, cette fois avec le bon élément résolu (confirmé par le message d’erreur citant le bon outerHTML) — donc force seul n’a pas suffi à rendre l’élément cliquable dans ce contexte <dialog> précis.

Contournement appliqué : abandon de cliquer/force pour cette famille d’éléments, bascule complète sur evaluer JS (showModal() + checkbox.checked = true + dispatchEvent(change) + submitButton.click() dans un seul script). Fonctionne à 100 % sur 4 répétitions (déconnexion de 4 domaines successifs, même dialogue).

Non investigué ce soir : si force: true échoue spécifiquement sur les éléments enfants d’un <dialog> ouvert par script (par opposition à un <dialog> ouvert par un vrai clic utilisateur), ou si c’est un problème plus général de timing entre showModal() et la disponibilité du layout au moment du clic suivant dans la même action list. Reproductible avec le dialogue de suppression de domaine Sillage (?vue=reglages&client=..., section Domaines, bouton « Supprimer » sur une carte domaine).

Confirmation du même pattern (12/07/2026, Diwall v1.20.0) : reproduit à l’identique sur un élément non lié à un <dialog> — un toggle switch CSS (case à cocher masquée visuellement, <label> custom par-dessus) pour activer la réplication automatique. cliquer avec force: true échouait de la même façon (« element outside of viewport » / not visible). Même contournement, même succès : evaluer JS direct (checkbox.checked = true + dispatchEvent('change')). Élargit la portée de FR-81 : le problème n’est pas spécifique à showModal(), mais à toute la famille des éléments interactifs masqués visuellement au profit d’un habillage CSS custom (dialog, toggle switch, probablement aussi les selects stylés). Renforce la recommandation d’escalade déjà formulée : documenter evaluer JS comme option par défaut, pas seulement de secours, pour toute cette famille.


FR-82 — boussole.session_derive a orienté un diagnostic vers une fausse piste (session expirée) alors que la cause réelle était une erreur HTTP 500 applicative masquée

Description : diagnostic d’un bouton d’action délégué (« Sauvegarder maintenant », espace client L2 de Sillage) qui redirigeait systématiquement vers la page de login au lieu d’exécuter l’action. Le signal boussole.session_derive de Diwall a orienté l’investigation vers l’hypothèse « session L2 expirée / cookie perdu entre deux étapes » — plusieurs cycles de test (--reprendre-session/--sauver-session, vérification document.cookie, reproduction en un seul appel Playwright continu pour éliminer une perte de cookie inter-process) ont été dépensés sur cette piste avant qu’elle ne soit écartée.

Cause réelle (trouvée hors Diwall, par curl brut reproduisant le POST exact) : un bug applicatif Sillage à deux étages — un formulaire HTML avec action="?" qui effaçait un paramètre GET nécessaire à l’authentification côté serveur, puis (une fois ce premier bug corrigé) une fonction PHP non chargée provoquant une erreur HTTP 500 masquée en page blanche côté client (display_errors=0, comportement de production correct mais qui cache le signal). Le symptôme observable (redirection vers login) était identique à une vraie expiration de session — mais la cause n’avait rien à voir avec la session elle-même.

Ce qui a permis de sortir de la fausse piste : abandon de Diwall pour cette étape précise, curl manuel reproduisant le POST exact — la distinction HTTP 500 vs HTTP 302 (redirection d’auth) a immédiatement écarté l’hypothèse session.

Non investigué ce soir : si session_derive se base sur un pattern générique (« retour inattendu vers une page de login ») sans distinguer le code HTTP réellement reçu, ou sur un autre signal. Si c’est le cas, le signal pourrait être affiné pour distinguer un vrai 302 d’authentification d’un flux qui ressemble à une perte de session vue depuis le DOM final (page de login affichée) mais qui provient d’une erreur serveur en amont. Suggestion : exposer le code HTTP de la dernière réponse à côté du signal session_derive, pour que l’appelant puisse écarter rapidement l’hypothèse session si le code n’est pas 302/401/403.


FR-83 — --sauver-session/--reprendre-session ne préserve pas l’état DOM transitoire (dialog ouvert, <select> positionné) entre deux invocations séparées

Description : test d’un formulaire dans un <dialog> (sélection source/destination pour une action de duplication, Sillage). Séquence en plusieurs invocations séparées de shot.py --reprendre-session : (1) clic pour ouvrir le dialog, (2)/(3) positionnement de deux <select> via evaluer + dispatchEvent(change), vérifié correct par lecture DOM immédiate, (4) clic sur le bouton de soumission dans une quatrième invocation séparée. Le clic a réussi (succes: true) mais a soumis les valeurs par défaut du HTML, pas celles positionnées à l’étape (2)/(3) — le dispatcher a reçu l’inverse de ce qui était attendu (source/dest inversés).

Cause : --sauver-session/--reprendre-session restaure l’état de la session HTTP (cookies), mais chaque invocation recharge la page depuis zéro — un <dialog> ouvert par un clic précédent et des valeurs de <select> positionnées par evaluer sans soumission ne survivent pas au rechargement. Le clic de l’étape (4) a donc cliqué un bouton de soumission bien réel mais à l’intérieur d’un dialog rouvert avec ses valeurs HTML par défaut, sans que rien ne le signale (succes: true, aucune erreur).

Contournement appliqué : regrouper ouverture + positionnement + vérification + soumission dans une seule invocation avec --actions (liste d’actions), au lieu de --action unique répété sur plusieurs appels --reprendre-session. Fonctionne à 100 % une fois regroupé — reproductible et vérifié par lecture DOM juste avant la soumission, dans le même appel.

Conséquence potentiellement grave évitée de justesse : dans ce cas précis, l’inversion source/destination aurait pu écraser un domaine différent de celui visé si les deux domaines n’avaient pas eu un contenu déjà proche l’un de l’autre au moment du test — un appelant moins prudent (pas de vérification de l’état avant/après) aurait pu attribuer à tort le succès rapporté par Diwall (succes: true) à l’opération réellement voulue.

Suggestion d’escalade pour GUIDE_LLM_INTERACTIONS.md : documenter explicitement que tout état DOM non soumis (dialog ouvert, champ rempli, select changé) est perdu entre deux invocations séparées de --reprendre-session, même si la session HTTP reste valide — une séquence « ouvrir → remplir → soumettre » doit toujours être un seul appel --actions, jamais fragmentée en plusieurs --reprendre-session successifs quand une soumission de formulaire est en jeu.

Version : Diwall v1.20.0. Session Sillage — 12/07/2026.

Escalade utile pour GUIDE_LLM_INTERACTIONS.md : la note actuelle sur force: true et showModal() (ligne ~31, decision tree item 4) mériterait un avertissement — dans un <dialog> ouvert par script plutôt que par interaction utilisateur, préférer d’emblée evaluer JS pour toute la séquence plutôt que cliquer + force.

Version : Diwall v1.17.2. Session Sillage — 03/07/2026.


Session HOST_VPS — 13 juillet 2026 — Construction d’un dashboard Grafana (édition SPA, éditeur Monaco)

Première session de ce type sur le projet HOST_VPS : piloter Grafana via son interface web (édition de panneau, requêtes PromQL) plutôt qu’une simple validation de login. Plusieurs frictions inédites, propres à une SPA d’édition complexe.


Confirmation de FR-83 dans un contexte différent (dashboard non sauvegardé)

Même cause, autre manifestation : construire un panneau Grafana étape par étape avec un appel shot.py par étape et --sauver-session/ --reprendre-session entre chaque renvoyait systématiquement à l’écran « New dashboard » vierge — le panneau en cours d’édition n’existe que dans le DOM/state React de la session navigateur précise, jamais sauvegardé nulle part ailleurs. Même remède que FR-83 : tout le parcours (login compris) dans une seule invocation tant que l’objet n’est pas persisté côté serveur. Une fois le dashboard réellement sauvegardé (UID obtenu), reprendre une session pour la suite (ajouter un panneau à un dashboard existant) redevient fiable. Un détail nouveau à ajouter à la règle de FR-83 : la frontière n’est pas « formulaire vs pas formulaire » mais persisté vs non persisté — un dashboard non sauvegardé est un cas exactement aussi volatile qu’un dialog non soumis.

Version : Diwall v1.20.0. Session HOST_VPS — 13/07/2026.


FR-84 — cliquer_som avec un ID capturé avant une mutation DOM non-navigationnelle peut viser le mauvais élément

Description : dans l’éditeur de requête Grafana, un bouton bascule « Builder » / « Code » fait disparaître un toggle « Explain » situé juste avant dans le DOM. Un ID de bouton (« Run queries ») capturé avant ce basculement ne correspondait plus au bon élément une fois en mode Code (décalage d’un cran). Une tentative d’enchaîner plusieurs actions à la suite sans revérifier le SoM après le basculement a produit une fois un échec franc (« élément non trouvé ») et, plus inquiétant, une fois un comportement inattendu : retour sur l’écran de login après 13 actions apparemment réussies, sans message d’erreur avant l’échec final sur un ID suivant. Cause exacte non confirmée (peut-être un clic ayant atteint un élément de déconnexion par pur hasard de décalage d’ID) — non investigué davantage, pas reproduit une seconde fois après correction.

Contournement appliqué : ne jamais réutiliser un ID de SoM au-delà d’une action qui modifie potentiellement le DOM, même sans navigation complète — un simple toggle suffit. Recapturer le SoM immédiatement après toute mutation, avant de s’en servir pour cibler l’action suivante.

Version : Diwall v1.20.0. Session HOST_VPS — 13/07/2026.


FR-85 — Éditeur Monaco (role="code") : remplir_som sur l’ID visible échoue silencieusement, remplir par sélecteur générique fait un strict-mode violation

Description : le champ de requête PromQL de Grafana (mode “Code”) est un éditeur Monaco (VS Code), rendu comme <div role="code" class="monaco-editor..."> avec un <textarea> caché à l’intérieur pour la saisie clavier réelle. Le SoM capturait cet élément comme un BUTTON au texte vide et non interactif au sens standard — remplir_som dessus ne fonctionnait pas. Tentative suivante avec cliquer sur .monaco-editor : échec strict-mode (2 éléments correspondent — l’éditeur réel et un widget de renommage Monaco caché ailleurs sur la page, tous deux avec la classe monaco-editor).

Contournement appliqué : reconnaissance ciblée via evaluer (document.querySelector('[role="code"] textarea')) pour confirmer le sélecteur précis, puis remplir (pas remplir_som) directement sur [role="code"] textarea — plus spécifique que .monaco-editor textarea, lève l’ambiguïté. Fonctionne du premier coup une fois le bon sélecteur identifié ; le texte apparaît avec la coloration syntaxique normale, confirmant qu’il passe bien par le modèle interne de Monaco et pas par un simple value= cosmétique.

Suggestion pour GUIDE_LLM_INTERACTIONS.md : ajouter un cas « éditeurs de code embarqués (Monaco, CodeMirror) » — SoM les numérote souvent mal (pas un vrai <input>/<textarea> visible), et [role="code"] textarea (ou équivalent CodeMirror .cm-content) est plus fiable qu’un sélecteur de classe générique, sujet à collision avec des instances cachées.

Version : Diwall v1.20.0. Session HOST_VPS — 13/07/2026.


FR-86 — Aucune prise en charge native du challenge HTTP Basic Auth (401)

RÉSOLU en v1.21.0 (15/07/2026). --http-credentials résout désormais les identifiants HTTP Basic depuis le vault et les injecte au contexte navigateur, scopés à l’origine de la cible. Validé contre une cible Basic Auth réelle (pas seulement une fixture locale) : succès dès le premier essai. Détail complet : docs/GUIDE_LLM_SESSIONS.md section --http-credentials, docs/JOURNAL.md session 55.

Description (constat d’origine, avant résolution) : deux interfaces d’administration auto-hébergées (un tableau de bord d’observabilité et un service de métriques associé) étaient protégées par un basic_auth Caddy (couche réseau, avant même l’application). shot.py --help passé en revue intégralement : aucune option pour fournir des identifiants HTTP Basic (pas de --http-credentials, pas de verbe d’action dédié). Le mécanisme de vault (--secrets, remplir_som + vault_cle) est conçu pour des formulaires web, pas pour un challenge navigateur natif qui bloque la page avant même son rendu.

Contournement appliqué : tunnel SSH local (ssh -L <port>:127.0.0.1:<port> __HOST_VPS__) vers les ports internes des applications, pour les valider directement sans jamais faire transiter le mot de passe basicauth par une URL en clair. Fonctionne bien pour un service interne joignable en SSH, mais n’aide en rien si la cible n’est que publique (pas de SSH possible vers le backend).

Évolution livrée (v1.21.0) : --http-credentials résout les identifiants depuis le vault (même discipline que remplir_som + vault_cle, jamais en clair dans les arguments CLI) et les injecte à la création du contexte navigateur (browser.new_context(http_credentials=…)), scopés à l’origine de la cible. Revalidé contre les deux interfaces d’origine de ce constat — succès dès le premier essai avec le mode par défaut le plus sûr (send: "unauthorized").

Version : Diwall v1.20.0 (constat). Résolu en v1.21.0. Session __HOST_VPS__ — 13-15/07/2026.


Ce qui a bien fonctionné, à noter aussi

  • Une fois la discipline « reconnaissance avant mutation » réellement suivie (capturer le SoM, l’inspecter, agir, plutôt qu’enchaîner à l’aveugle), zéro nouvel échec sur le reste du parcours — y compris la pose du titre du panneau via manipulation directe du setter React natif (evaluer + Object.getOwnPropertyDescriptor(...).set), nécessaire car ce champ ignore un input/change event synthétique simple sans passer par le setter natif de HTMLInputElement.
  • La preuve visuelle (capture) a été le vrai livrable utile face à l’opérateur : un curl à 200 n’aurait rien dit sur si Grafana affichait réellement un dashboard fonctionnel avec de vraies données après la coupure de l’OAuth Plesk mort.

Version : Diwall v1.20.0. Session HOST_VPS — 13/07/2026.