Dev #5

Merged
w-vandal merged 16 commits from dev into main 2026-08-30 09:29:41 +00:00
Owner
No description provided.
w-vandal added 16 commits 2026-08-30 09:29:33 +00:00
Le modal était placé en dehors de #pageChrome ET de <main> — or pjax.js
(swapDocument()) ne remplace jamais que ces deux conteneurs à chaque
navigation, jamais le body entier. La redirection qui suit la
confirmation de la 2FA passe par une navigation pjax (fetch), pas un
vrai rechargement de page : le HTML du modal était bien renvoyé par le
serveur, mais jamais copié dans le DOM réellement affiché, donc jamais
visible pour un utilisateur qui vient de s'inscrire.

Déplacé à l'intérieur de <main class="content"> : fait maintenant partie
de innerHTML remplacé par pjax.js à chaque navigation, comme le reste du
contenu de page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Depuis /profile :
- "Codes de récupération 2FA" : régénère les 10 codes (invalide les 10
  précédents d'un coup, voir auth.generate_recovery_codes) et les affiche
  une seule fois via le même modal qu'à l'inscription (session flash,
  core/recovery_codes_flash.py) — mot de passe actuel requis.
- "Adresse email" : change l'adresse (mêmes règles qu'à l'inscription —
  format valide, unicité — voir auth/email_validation.py, désormais
  partagé avec create_user.py au lieu d'être dupliqué). Pour un compte
  "user", le dossier de son unique projet porte le nom de son adresse
  (project_slug = slugify(email)) : il est renommé pour suivre le
  changement (db/games/move_game.py, même logique d'unicité par suffixe
  que create_game()), sans quoi project_slug ne correspondrait plus à
  aucun dossier réel. Un "admin" n'a pas de project_slug dédié : rien
  n'est renommé pour ce rôle.

18 nouveaux tests (tests/test_profile.py) : mauvais mot de passe refusé
pour les deux actions, mauvais format/email déjà pris refusés, dossier
bien renommé et project_slug mis à jour, connexion possible avec la
nouvelle adresse, anciens codes de récupération bien invalidés après
régénération, modal jamais réaffiché deux fois.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Les 5 sections (email, informations, mot de passe, codes de récupération,
suppression du compte) tenaient les unes sous les autres dans une seule
colonne étroite (560px) avec le padding/espacement par défaut de Bulma —
imposait de scroller pour tout voir. Réorganisé en grille CSS 2 colonnes
(container élargi à 980px, static/style.css : .profileGrid/.profileCol/
.profileBox) : compte/infos/suppression à gauche, sécurité (mot de passe,
codes de récupération) à droite. Champs resserrés (labels remplacés par
des placeholders, is-small partout, marges réduites), jauge de force du
mot de passe sur une ligne (.passwordChecklist-compact). Repasse en une
seule colonne sous 720px de large.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Met en place les fondations du template Bulma décrit dans
regles/FORGE_ENGINE_TEMPLATE_BULMA.md : palette orange/ambre Forge
(--accent:#ff5f2e/--accent-2:#ffb020), typographie, rayons — appliqués
en recompilant Bulma lui-même plutôt qu'en le surchargeant après coup en
CSS, pour recolorer automatiquement ses composants internes (tags,
notifications, dropdowns...) sans avoir à les surcharger un par un.

- Bulma 0.9.4 vendoré en local (styles/bulma/, source Sass classique
  $variable + @import) — PAS 1.0.2 (la version jusqu'ici en CDN) : 1.0.2
  utilise le nouveau système de modules @use/@forward, que libsass (choisi
  pour rester 100% Python, sans Node/npm) ne sait pas compiler (testé :
  il ignore silencieusement le @use au lieu de le traiter). 0.9.4 est la
  dernière version compatible avec libsass et couvre à l'identique tous
  les composants utilisés ici (boutons, tableaux, onglets, modales,
  formulaires, navbar).
- requirements.txt : +libsass (pip pur, aucun binaire/Node.js).
- styles/forge-theme.scss (nouveau, point d'entrée) : variables Sass
  Bulma ($primary, $radius...) posées avant l'import, tokens Forge exposés
  en :root (--forge-bg, --accent, --gradient, --status-*...) avec des
  alias vers les noms de variables déjà utilisés par tout le CSS custom
  existant (--bg/--panel/--border/--text/--danger...) — pas besoin de
  renommer les ~600 lignes de règles déjà écrites, seules leurs VALEURS
  changent.
- styles/forge-custom.scss : ancien static/style.css, structurellement
  inchangé — seuls les hex/rgba en dur qui échappaient aux variables
  (ancien accent bleu #5b8cff, danger #e2685f, couleurs de types de
  nœuds du graphe de logique, fond du QR code recovery...) sont
  remplacés par les tokens de la charte.
- build_css.py (nouveau) : compile styles/forge-theme.scss en
  static/style.css via libsass — un seul fichier, un seul <link>
  inchangé dans les templates, à relancer manuellement après toute
  modification sous styles/.
- base.html/play.html : suppression du <link> CDN Bulma (auto-hébergé
  désormais), ajout d'un favicon (absent jusqu'ici) et du vrai logo Forge
  dans la navbar (assets/*.svg copiés dans static/branding/, seul dossier
  réellement servi par Flask).

197 tests toujours verts (aucune assertion sur des valeurs CSS).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- styles/forge-custom.scss : .content-wide (déjà utilisée par
  game_dashboard.html/index.html) passe de 1080px à 1600px, pleine
  largeur pour les écrans de gestion (§5) — la largeur du bloc <main> par
  défaut (760px, hérité par les pages d'auth/profil qui ne posent pas
  cette classe) reste inchangée, ces pages veulent justement rester
  étroites et centrées.
- Pages d'authentification (login/register/2FA×2/mot de passe oublié/
  réinitialisation) : le style="max-width:NNNpx" répété sur chacune est
  remplacé par les classes partagées .authScreen/.authCard(-wide), titre
  H1 en dégradé signature .forge-gradient-title — seul endroit du site,
  avec le logo, autorisé à l'utiliser (§3/§5). Grille de fond fine sur
  body.authBody, elle aussi réservée aux zones hero et absente des écrans
  de travail.
- Logique JS inchangée (jauge de mot de passe dans register.html/
  reset_password.html) — uniquement des classes/structure autour.

197 tests toujours verts, JS des pages modifiées revérifié (node --check).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- index.html ("Mes jeux") : le <h1> nu est remplacé par l'en-tête de page
  compact du §5 (.dashPanelHeader, déjà utilisé ailleurs pour ce
  patron) — titre + badge du nombre de jeux (.forge-badge) sur une seule
  ligne. Pas de second CTA : le formulaire de création reste dans sa
  colonne dédiée, un bouton de plus ferait doublon.
- game_dashboard.html : ajout du même en-tête, absent jusqu'ici (la page
  démarrait directement sur la barre d'onglets) — nom du jeu + chemin du
  dossier, pour savoir sur quel jeu on se trouve sans avoir à regarder
  l'URL.
- styles/forge-custom.scss : .content-objectEdit > h1/.hint/form/
  .warningBanner (règle qui fige la hauteur des enfants directs non
  extensibles dans la mise en page flex de l'accueil) étendue à
  .dashPanelHeader, qui remplace maintenant le <h1> direct.
- profile.html : déjà conforme (rayons de boîte/bouton, danger tokenisé)
  depuis le passage global des tokens en Phase A — aucun changement
  supplémentaire nécessaire.

197 tests toujours verts ; vérification ciblée (fixture jetable, supprimée
ensuite) confirmant l'en-tête et le JS du tableau de bord.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'essentiel du re-skin de l'éditeur de scène (onglets, panneaux flottants,
groupes de propriétés, boutons, galerie d'icônes, menu contextuel, modales)
était déjà obtenu automatiquement par le passage global des tokens en
Phase A — tout ce CSS custom consommait déjà les mêmes variables
(--panel/--border/--accent...) que le reste de l'app. Reste ce sweep
ciblé des dernières couleurs codées en dur qui échappaient aux tokens :

- screen_edit.html : bandeau "élément de jeu réutilisable" (fond/bordure
  panneau en dur), avertissement de suppression de nœud de logique
  (fallback --danger périmé), séparateur de clause de condition (gris
  clair #ccc, incohérent sur un thème exclusivement sombre) — tous
  reliés à var(--panel)/var(--border)/var(--danger). Les couleurs de
  swatch par défaut des actions "changer la couleur d'un élément"
  (#5b8cff, #ff0000) sont volontairement laissées telles quelles : ce
  sont des valeurs de CONTENU (le jeu du client), pas du chrome Forge.
- play.html : #emptyState (message "aucun écran" généré par le moteur,
  pas du contenu du jeu) relié à var(--forge-text-muted). Le rendu du
  jeu lui-même (fond du cadre, bordure des champs de saisie posés par le
  client sur ses écrans) reste strictement inchangé, conformément à la
  distinction actée avec l'utilisateur entre chrome de l'outil et
  contenu du jeu créé.
- Aucun changement à la disposition (canvas, floatPanel, builder3) —
  conforme à l'exemption du §5/§9 du document de règles.

197 tests toujours verts ; JS de screen_edit.html (très long) revérifié
via une extraction jetable + node --check, comme pour les phases
précédentes.

Termine la refonte design system en 4 phases (voir
regles/FORGE_ENGINE_TEMPLATE_BULMA.md) : Bulma auto-hébergé compilé via
Sass avec la palette Forge, chrome global et pages d'authentification,
en-têtes de page des écrans de gestion, éditeur de scène et aperçu
jouable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'utilisateur a signalé des bugs réels en jeu après la refonte design
system : jauges qui ne se remplissent plus, conditions de visibilité
cassées sur une carte (enveloppe ouverte/fermée affichées en même temps).
Racine du problème : Phase A avait basculé Bulma de 1.0.2 (CDN d'origine)
vers 0.9.4, seule version compatible avec libsass (1.0.2 utilise le
système de modules @use/@forward que libsass ne sait pas compiler). Or
screens/rendering/render_jauge.py pilote le remplissage d'une jauge en
fixant en ligne --bulma-progress-value-background-color — une variable
CSS qui n'existe QUE dans le nouveau système de theming de Bulma 1.x,
absente de 0.9.4. Aucune perte de données : les champs d'objet étaient
toujours intacts en base (vérifié directement sur projects/test/game.db)
— uniquement un problème de rendu/comportement en jeu.

Correction : revient à Bulma 1.0.2, vendoré tel quel et non modifié
(static/vendor/bulma.min.css, ~677 Ko, auto-hébergé — toujours aucune
dépendance CDN). Bulma 1.x expose déjà tout son thème via de vraies
variables CSS (--bulma-primary-h/-s/-l, --bulma-radius...), justement
conçues pour être surchargées après coup SANS recompilation Sass —
styles/bulma-override.css les redéfinit avec la palette Forge (teintes
HSL calculées à partir des couleurs de la charte). build_css.py devient
un simple concaténage de 4 fichiers (bulma.min.css + bulma-override.css
+ forge-tokens.css + forge-custom.css), plus besoin de libsass ni
d'aucun compilateur — supprimé de requirements.txt. styles/bulma/ (source
Sass 0.9.4 vendorée en Phase A) et styles/forge-theme.scss supprimés.

Nouvelle règle ajoutée en commentaire dans bulma-override.css : ne plus
jamais changer de version de Bulma sans `grep -rn "\-\-bulma-" screens/
templates/` d'abord — cette dépendance n'est pas que visuelle.

197 tests toujours verts (ils ne couvrent que le HTML généré, jamais le
rendu réel — c'est pour ça que cette régression n'avait pas été détectée
avant que l'utilisateur ne la signale en jouant pour de vrai).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ce bug n'est PAS lié à la refonte design system de cette session — le
fichier concerné (templates/play.html) n'avait plus été touché depuis
une session précédente (commit ed4dd78), bien avant les changements de
CSS/Bulma. Vérifié en cherchant "les données ont-elles été perdues ?" :
non, tous les objets/champs restent intacts en base (projects/test/game.db).

Root cause réel : refreshRuntimeData()::findCommentMarkedDescendants()
ne repérait, dans le HTML fraîchement régénéré après une action
"Modifier une donnée", que les éléments qui REDEVIENNENT VISIBLES sous
condition (repérés via le commentaire "<!--visibilityGated-->" posé après
leur contenu réel par _mark(), voir render_element_html.py) — jamais ceux
qui REDEVIENNENT CACHÉS, dont le placeholder ('<div class=
"visibilityGated" ... style="display:none;">') n'a pas de commentaire à
sa suite, juste une classe sur lui-même. Résultat : l'élément qui
redevient visible est bien patché dans le DOM, mais l'ancien élément
visible n'est jamais retiré — les deux restent affichés en même temps
(ex. "enveloppe fermée" jamais masquée à côté de "enveloppe ouverte" qui
apparaît après un clic).

Corrigé en faisant aussi reconnaître, dans findCommentMarkedDescendants(),
la classe "visibilityGated" directement sur la balise (cas caché), en
plus du commentaire (cas visible) — les deux sens du bascule sont
maintenant retrouvés et patchés.

tests/test_visibility_toggle_markers.py (nouveau) verrouille le contrat
côté serveur dont dépend ce correctif JS : un élément cité verrouille que
l'élément CACHÉ porte bien la classe (jamais de commentaire), l'élément
VISIBLE porte bien le commentaire (jamais la classe), et que ces rôles
s'inversent correctement quand la donnée change. 199 tests au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Étape 1 de la fonctionnalité "Publier un jeu en exécutable autonome" :
un jeu exporté devra fonctionner sans AUCUNE connexion internet, ce qui
suppose d'abord que l'app elle-même n'ait plus aucune dépendance CDN —
Bulma l'était déjà (refonte design system), il ne restait que Google
Fonts et animate.css, chargés par templates/play.html ET
templates/screen_edit.html (aperçu du canevas dans l'éditeur).

GOOGLE_FONTS_LINK (screens/widgets/font_options.py) était une constante
FIXE (4 polices, 2 graisses chacune, jamais dépendante du jeu en cours) :
téléchargées une fois pour toutes (45 fichiers woff2, ~1.1 Mo au total
avec la couverture cyrillique/vietnamienne incluse) dans
static/vendor/fonts/, avec un fonts.css régénéré à partir du CSS
officiel de Google Fonts mais pointant vers les fichiers locaux. Même
chose pour animate.min.css 4.1.1 (static/vendor/animate.min.css).

routes/play/game_play.py et routes/screens/screen_edit.py ne passent
plus google_fonts_link aux templates (devenu inutile, les deux pages
chargent directement les fichiers locaux) ; GOOGLE_FONTS_LINK est retiré
de screens/widgets/font_options.py et screens/__init__.py (plus aucun
appelant).

tests/test_animations.py : les deux tests qui vérifiaient la présence
du lien CDN vérifient maintenant la présence du fichier local
(animate.min.css) — renommés en conséquence.

199 tests toujours verts. Reste à faire pour la fonctionnalité complète :
empaquetage Python portable + serveur minimal + bouton "Publier".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fonctionnalité mise de côté depuis le tout début de ce chantier
("jouable en toute autonomie"). Nouveau bouton "📦 Publier" dans la barre
de navigation du jeu (après "▶️ Jouer") : construit un zip contenant un
Python portable + Flask embarqués, une copie figée du moteur de rendu
(screens/db/filters + le strict minimum de core/) et des données du jeu
(game.db + uploads/), lancé en double-cliquant sur run.bat — aucune
installation requise, 100% hors-ligne (voir le commit précédent qui a
auto-hébergé polices/animate.css, dernière dépendance CDN de l'app).

Exploration préalable a confirmé que screens/ et db/ sont déjà
totalement découplés de auth/routes/core (seul lien : un try/except
optionnel dans db/connection.py) et que templates/play.html n'appelle
que 4 endpoints "purs" (aucune logique d'auth mélangée dedans) — ce qui
a permis de réutiliser ces routes quasiment telles quelles dans un
mini-serveur Flask séparé plutôt que de les réécrire.

- db/constants.py : PROJECTS_DIR devient surchargeable via
  FORGE_PROJECTS_DIR (même schéma que auth/connection.py) — le serveur
  joueur autonome pointe ainsi vers son propre dossier "projects/"
  embarqué.
- publish/vendor_runtime.py : télécharge (une fois par poste, mis en
  cache sous data/publish_vendor/ — déjà ignoré par git) le ZIP Python
  embeddable officiel (python.org) et vendore Flask via pip --target ;
  fonctions séparées et mockables pour ne jamais déclencher de vrai
  téléchargement dans les tests.
- publish/player_app_template.py : mini-Flask autonome, réutilise
  core/flask_app.py et core/jinja_filters.py tels quels (SLUG figé en
  dur au moment de la publication, csrf_token() factice puisqu'aucune
  session n'existe dans cet export). sys.path doit être complété
  manuellement au démarrage : le python311._pth de la distribution
  embeddable ne référence que le dossier de python.exe lui-même, jamais
  celui du script lancé.
- publish/build_package.py : assemble le zip dans un dossier temporaire
  (jamais les vrais fichiers de l'app), copié/nettoyé après envoi de la
  réponse HTTP (routes/publish/publish_game.py, déjà protégée par la
  garde d'accès existante — aucune vérification supplémentaire).
- templates/base.html : bouton + modale (avancement séquentiel, jamais
  de suivi serveur réel — le build est rapide) qui déclenche le
  téléchargement du zip via un blob, comme la modale des codes de
  récupération déjà en place (posée À L'INTÉRIEUR de <main> pour que
  pjax.js la remplace et rejoue son script à chaque navigation).

Vérifié pour de vrai (pas seulement via les tests) : zip construit,
extrait, lancé avec le Python embeddable réel — /, /game/test/play,
/game/test/runtime-payload et /static/style.css répondent tous 200.

tests/test_publish.py (nouveau, vendor mocké) : structure du zip,
SLUG correctement substitué, route protégée par la même isolation par
projet que le reste de l'app. 203 tests au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Première moitié de la fonctionnalité "événements" (NEED_ACTION et
autres) : une entité game-wide (nom, description, "a un paramètre
élément" oui/non), déclenchable comme nouvelle action du graphe de
logique depuis n'importe quelle scène/modèle, et écoutable comme
nouveau type de déclencheur depuis n'importe quel autre. L'UI (nouvel
onglet "Événements" dans screen_edit.html, formulaires de nœud, exécution
côté client dans play.html) suit dans un commit séparé.

- db/custom_events/ (calqué sur db/global_vars/) : CRUD de la table
  _custom_events (nom unique, description, has_element_param).
  create_custom_event est idempotent par nom (même convention que
  create_global_variable) — sans risque en cas de double soumission.
- screens/flow/ : 3 nouvelles colonnes sur _flow_nodes
  (trigger_custom_event_id/target_custom_event_id : quel événement un
  nœud écoute/déclenche ; target_element_from_event : indicateur
  réutilisable par n'importe quel nœud Action utilisant déjà
  target_element_id, pour résoudre "l'élément transmis par l'événement
  en cours" au lieu d'une cible fixe — contourne la contrainte de clé
  étrangère de target_element_id, qui empêche d'y stocker un sentinel
  comme EVENT_ROW_ID directement). Nouveau trigger_event "evenement" et
  action_type "declencher_evenement".
- screens/custom_events/ (PAS dans db/, même séparation que
  screens/elements/delete_element.py) : delete_custom_event, la SEULE
  suppression d'entité game-wide du moteur à vraiment cascader (demande
  explicite) — supprime tous les nœuds/arêtes qui référencent
  l'événement, sur TOUTES les scènes ET tous les modèles à la fois
  (aucun filtre screen_id nécessaire : un modèle est un écran caché,
  même table _flow_nodes). list_custom_event_usages : où un événement
  est écouté/déclenché, pour l'onglet Événements à venir.
- routes/custom_events/ : CRUD monté sous /game/<slug>/events/...,
  redirige vers l'éditeur de scène/modèle d'origine (screen_id transmis
  par le formulaire) avec l'onglet "events" à ouvrir.

tests/test_custom_events.py (nouveau) : idempotence à la création,
usages détectés sur deux écrans différents, suppression qui retire bien
les DEUX nœuds (un sur une vraie scène, un sur un modèle/écran caché)
en une seule opération, sans toucher aux écrans eux-mêmes. 207 tests au
total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deuxième moitié de la fonctionnalité "événements" (voir le commit
précédent pour le backend) : un nouvel onglet "📣 Événements" dans
screen_edit.html (scène ET modèle, même route) pour créer/modifier/
supprimer un événement (nom, description, "a un paramètre élément" oui/
non) et voir où il est déjà écouté/déclenché ; deux nouveaux types de
nœud dans le graphe de logique ; exécution réelle côté client
(play.html).

- templates/screen_edit.html :
  - Onglet Événements : barre de création + tableau (patron
    form/name="..." + attribut `form="eventEditFormN"` déjà utilisé par
    l'onglet Variables du tableau de bord, pour éditer plusieurs champs
    d'une ligne sans emboîter un <form> dans un <tr>). Colonne "Utilisé
    par" avec liens directs vers les écrans/modèles concernés
    (list_custom_event_usages, déjà en place côté backend).
  - Nœud Déclencheur "Sur un événement personnalisé" : un simple sélecteur
    d'événement (trigger_custom_event_id) — ni élément ni écran, retrouvé
    par un scan global côté client (comme "À l'affichage de l'écran").
  - Nœud Action "Déclencher un événement" : sélecteur d'événement
    (target_custom_event_id), avec élément/ligne concernés affichés
    seulement si l'événement a has_element_param (réutilise le sélecteur
    d'élément existant + "Ligne cliquée" pour la ligne, pas de définition
    d'objet ici donc pas de liste de lignes fixes possible).
  - Nœud Action "Modifier un élément" : nouvelle case "Utiliser l'élément
    transmis par l'événement en cours" (target_element_from_event) — v1,
    seule cette action l'expose (extensible plus tard sans nouveau
    changement de schéma).
  - nodeLabel()/submitNodeForm()/toggleFlowTriggerFields()/
    toggleFlowActionFields() étendus en conséquence, CUSTOM_EVENTS_MAP
    (id -> nom/has_element_param) exposé côté JS pour le rendu des
    libellés et l'affichage conditionnel des champs paramètre.

- templates/play.html :
  - window.dispatchGameEvent(eventId, elementId, rowId) : scan de
    gameData.flows (toutes scènes ET modèles à la fois, même principe que
    findTriggerNode()/runScreenShowTriggers()) pour trouver chaque
    écouteur, pose window.lastEventParams puis exécute son graphe
    (runFlowFrom) — au même titre que window.lastClickedRowId pour "Ligne
    cliquée".
  - runActionNode : nouvelle branche "declencher_evenement" (résout
    CLICKED_ROW_ID pour la ligne transmise, comme "Modifier une donnée")
    ; branche "modifier_element" étendue pour résoudre dynamiquement
    l'élément depuis window.lastEventParams quand
    target_element_from_event est actif.
  - readFieldValue (évaluation de condition) et l'appel serveur de
    "Modifier une donnée" (routes/flow/flow_node_run_data.py) résolvent
    désormais aussi EVENT_ROW_ID (-2), au même endroit que CLICKED_ROW_ID
    (-1) déjà en place.

- routes/screens/screen_edit.py : passe custom_events/
  custom_event_usages/custom_events_map_json au template (même patron
  que global_variables déjà threadé pour le sélecteur de variable dans
  le formulaire de Condition).

Nouveaux tests dans tests/test_custom_events.py : forme exacte du
payload runtime exposé au JS (types entiers, pas des chaînes — une
comparaison stricte "===" échouerait silencieusement sinon),
target_element_from_event bien persisté, résolution serveur d'
EVENT_ROW_ID depuis le corps de la requête. 210 tests au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signalé par l'utilisateur : quand "Modifier un élément" utilise la case
"Utiliser l'élément transmis par l'événement en cours"
(target_element_from_event), et que cet élément vit à l'intérieur d'un
Répéteur, la résolution ne ciblait jamais que le PREMIER élément
correspondant trouvé dans toute la page — jamais forcément la bonne
ligne.

Vérifié directement sur un rendu réel : chaque ligne d'un Répéteur
rejoue le MÊME modèle (voir render_repeater.py), donc le MÊME
data-element-id se répète à l'identique sur CHAQUE .repeaterItem — seul
data-row-id (posé sur l'enveloppe .repeaterItem) distingue réellement
une ligne d'une autre. Un simple
document.querySelector('[data-element-id]') global tombe donc toujours
sur la première ligne rencontrée dans le DOM, sans rapport avec la ligne
réellement transmise par l'événement (window.lastEventParams.row_id).

Corrigé : quand l'événement transmet aussi une ligne, la recherche est
désormais scopée à l'intérieur du .repeaterItem[data-row-id=...]
correspondant avant d'y chercher l'élément — sinon (élément fixe, ou
événement sans paramètre de ligne), le comportement global d'avant reste
inchangé.

210 tests toujours verts (ce correctif est purement côté client, jamais
couvert par les tests automatisés — vérifié manuellement via un rendu
réel confirmant la structure .repeaterItem/data-row-id décrite
ci-dessus).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Retour de l'utilisateur sur le premier jet : "Déclencher un événement"
ne doit JAMAIS faire choisir un élément — c'est une notification pure,
rien de plus. C'est à l'ÉCOUTEUR (déclencheur "Sur un événement
personnalisé" → condition → action) de décider quoi faire ensuite, avec
ses réglages habituels (cible fixe, "Ligne cliquée"...), jamais à
l'événement de transporter un paramètre.

Retire donc tout le mécanisme de transmission ajouté au tour précédent
(has_element_param, target_element_from_event, EVENT_ROW_ID,
window.lastEventParams) :

- db/custom_events/ : _custom_events perd sa colonne has_element_param —
  un événement n'est plus qu'un nom + une description.
- screens/flow/ : retire target_element_from_event (colonne ajoutée par
  ALTER TABLE, laissée inerte sur les bases déjà migrées — sans
  conséquence, plus jamais lue ni écrite) et la constante EVENT_ROW_ID.
- routes/flow/flow_node_run_data.py : retire la résolution EVENT_ROW_ID,
  revient à sa forme d'origine (seul CLICKED_ROW_ID reste géré).
- templates/screen_edit.html : le nœud Action "Déclencher un événement"
  n'a plus qu'un sélecteur d'événement — plus de champs élément/ligne.
  Le nœud Action "Modifier un élément" perd la case "Utiliser l'élément
  transmis par l'événement en cours". L'onglet Événements perd la case
  à cocher "Paramètre" (création et édition).
- templates/play.html : window.dispatchGameEvent(eventId) ne prend plus
  que l'id de l'événement — scan global inchangé, mais ne pose plus
  aucun window.lastEventParams. modifier_element et readFieldValue
  reviennent à leur résolution d'origine (plus de branche event-aware).

208 tests au total (2 tests retirés, devenus sans objet : la
persistance de target_element_from_event et la résolution serveur
d'EVENT_ROW_ID).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diagnostic effectué directement sur projects/test/game.db de
l'utilisateur (suite à son signalement "je ne parviens pas à mettre fin
à la surbrillance") : deux nœuds Déclencheur distincts ("Au clic",
id=4 et id=42) référençaient le même trigger_element_id=65. La fonction
findTriggerNode() utilisait Array.find(), qui ne retourne que la
PREMIÈRE correspondance — le second nœud (celui qui devait couper la
surbrillance) n'était donc jamais exécuté, silencieusement, quel que
soit le graphe construit dans l'éditeur.

Ce n'est pas un bug lié aux événements personnalisés ni aux conditions
par variable (deux pistes explorées avant ce diagnostic) : c'est une
limitation générale du moteur, qui n'a jamais géré plus d'un déclencheur
"Au clic"/"Au survol"/"À la fin du survol" par élément.

Renomme findTriggerNode() en findTriggerNodes() (pluriel) : retourne
désormais TOUS les nœuds correspondants (élément + type d'événement),
et bindClicks()/bindHoverTriggers() exécutent chaque flow trouvé au lieu
de s'arrêter au premier.

Vérifié : 208 tests passent, syntaxe JS validée (script de play.html
rendu via le client de test puis node --check). Comme pour le reste du
graphe de logique côté client, ce changement n'est pas couvert par les
tests automatisés (pas de harnais navigateur/DOM) — vérification
manuelle recommandée sur le scénario réel de l'utilisateur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
w-vandal merged commit 1020aaaf2f into main 2026-08-30 09:29:41 +00:00
Sign in to join this conversation.