Commit Graph
95 Commits
Author SHA1 Message Date
williamandClaude Sonnet 5 5c069ae1fe Corrige LE vrai bug : un nom de champ mot-réservé SQL (ex. "order") faisait disparaître l'objet entier
Reproduit à l'identique le cas signalé (objet "dialog" avec les champs
order/spiker/text/level_id/parcour_id) : le champ "order" est un mot
réservé SQL — "CREATE TABLE dialog (order INTEGER, ...)" plante avec
"OperationalError: near \"order\": syntax error". Comme ce crash survient
APRÈS l'INSERT de la ligne _definitions mais AVANT le commit(), rien
n'était jamais persisté : l'objet ENTIER disparaissait, malgré des champs
parfaitement remplis — d'où "j'ai tout rempli comme il faut et aucun objet
n'est créé". Mon précédent correctif (champ "Relation" sans cible) était
réel mais ne couvrait pas ce cas précis.

Cause de fond : chaque nom de colonne (dérivé du nom de champ tapé par
l'utilisateur, via slugify) était interpolé TEL QUEL dans du SQL brut
(CREATE TABLE, INSERT, UPDATE, ALTER TABLE ADD/DROP/RENAME COLUMN) sans
jamais être encadré de guillemets — n'importe quel nom de champ qui soit
aussi un mot réservé SQLite (order, group, index, select, where, table,
key, default, check, references, unique...) déclenchait exactement le
même crash-et-perte-de-transaction, dans n'importe laquelle de ces
opérations.

Correctif général (pas un simple contournement pour "order") :
db/quote_ident.py encadre tout identifiant de colonne de guillemets
doubles (forme standard SQL, supportée par SQLite) — appliqué partout où
un nom de colonne utilisateur est interpolé dans du SQL brut :
create_definition, add_field_to_definition, delete_field, update_field
(RENAME COLUMN), insert_row, update_row, update_row_field,
rows_referencing. Les noms de TABLE n'ont pas besoin de cette protection
(table_name_for.py les préfixe toujours "obj_", donc jamais un mot réservé
à eux seuls).

Trois nouveaux tests (tests/test_reserved_sql_keyword_field_names.py) :
création avec un champ "order" + insertion/lecture/mise à jour d'une
ligne, renommage d'un champ vers/depuis un mot réservé ("group"), ajout
d'un champ "select" à un objet existant — les trois confirmés en échec
sur l'ancien code (même erreur reproduite) puis au vert avec le
correctif. 136 tests au vert au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 19:27:09 +02:00
williamandClaude Sonnet 5 c14f7e9ba5 Corrige le vrai bug : un champ "Relation" sans objet cible plantait toute la création
L'utilisateur avait raison de contester mon précédent correctif : le
problème n'était pas l'absence de champs. Reproduit précisément : créer un
objet avec un champ de type "Relation vers un autre objet" SANS avoir de
cible valide sélectionnée (ex. le tout premier objet créé dans un jeu — le
sélecteur "Objet lié" est alors vide, faute d'un autre objet à pointer)
plantait toute la requête avec une ValueError ("invalid literal for int()
with base 10: ''") dans create_definition() / add_field_to_definition()
(int(relation_definition_id) sans filet). Comme le crash survient APRÈS
l'INSERT de la ligne _definitions mais AVANT le commit(), rien n'était
jamais persisté (transaction perdue à la fermeture de la connexion) :
l'objet entier disparaissait, pas seulement son champ "Relation" — d'où
"le panneau recharge la page sans créer d'objet" alors que des champs
avaient bien été renseignés.

Correctif (routes, pas la couche db) : un champ "Relation" dont la cible
n'est ni choisie ni un id valide est maintenant simplement IGNORÉ (comme
une ligne sans nom, déjà le cas), dans les deux endroits qui construisent
ce payload :
- routes/objects/parse_field_rows.py (panneau "+ Nouvel objet")
- routes/objects/object_field_add.py (panneau "+ Ajouter un champ" d'un
  objet déjà créé — même risque de crash dans add_field_to_definition)
object_field_edit.py/update_field.py avaient déjà la bonne garde
("if relation_definition_id" avant le int()) — rien à y changer.

Deux nouveaux tests, confirmés en échec sur l'ancien code (git stash,
même ValueError reproduite) puis au vert avec le correctif. 133 tests au
vert au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 19:10:54 +02:00
williamandClaude Sonnet 5 6cf4fdbe72 Corrige la création d'objet sans champ : le panneau rechargeait la page sans rien créer
Bug rapporté : le panneau "+ Nouvel objet" du tableau de bord "recharge la
page sans créer d'objet". Cause : object_new exigeait "name AND fields"
pour créer quoi que ce soit — or le formulaire du panneau permet de taper
le nom et de cliquer directement "Créer l'objet" SANS avoir cliqué au
préalable "+ Ajouter un champ" (les champs se posent typiquement APRÈS,
depuis le panneau "Modifier un objet", workflow déjà supporté). Sans champ
soumis, la condition échouait, la route redirigeait silencieusement vers
le tableau de bord SANS créer l'objet ET sans le moindre message d'erreur
— vécu comme "un rechargement qui ne fait rien".

create_definition(fields=[]) fonctionne déjà très bien (crée juste une
table avec id/created_at, sans colonne "métier") : retiré l'exigence d'au
moins un champ, ne reste que "name" non vide.

Nouveau test (tests/test_object_new_without_fields.py), confirmé en échec
sur l'ancien code (git stash) puis au vert avec le correctif.
131 tests au vert au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 19:03:29 +02:00
williamandClaude Sonnet 5 7dd7551bc5 Corrige le texte illisible dans les boîtes de dialogue du jeu de démo
Pas un bug moteur cette fois : le moteur ne fige JAMAIS de couleur de
texte par défaut à la création d'un élément (voir
default_style_for_widget.py) — un titre/texte fraîchement posé retombe
donc sur le texte SOMBRE par défaut de Bulma, pensé pour un fond clair.
Mon script de démo ne posait jamais explicitement de couleur de texte sur
les titres/paragraphes à l'intérieur des boîtes de dialogue (fond sombre
volontaire) : le texte y était donc quasi invisible, ce qui donnait
l'impression trompeuse que "la boîte de dialogue est assombrie" — alors
que seul son texte, invisible par défaut sur fond sombre, l'était (la
boîte elle-même a bien sa couleur opaque demandée, #20263a/#1f3a24/
#2a2440, sans aucun voile supplémentaire dessus).

Ajout de la couleur de texte manquante (#f5f6fa) sur chaque titre/texte
posé dans une boîte de dialogue. Valeur volontairement différente de la
suggestion par défaut du panneau (#e8eaf0) : save_element_controls.py
ignore délibérément un réglage renvoyé identique à sa valeur par défaut
tant qu'il n'a jamais été personnalisé (même logique anti-figeage que
default_style_for_widget.py) — envoyer #e8eaf0 tel quel n'aurait donc eu
AUCUN effet, silencieusement (piège rencontré et documenté dans le script).

Jeu de démo régénéré (supprimé puis reconstruit) avec le correctif.
129 tests toujours au vert (script indépendant, aucun changement moteur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 18:52:17 +02:00
williamandClaude Sonnet 5 81c31a9c49 Corrige la boîte de dialogue (superposition) : un élément posé après elle s'affichait par-dessus
Bug visible sur le jeu de démo : le bouton "Clique-moi !" restait visible
ET cliquable AU-DESSUS du dialogue de bienvenue censé couvrir tout l'écran,
et la boîte de dialogue elle-même s'étirait bord à bord au lieu de rester
une boîte centrée lisible.

Cause (stacking context CSS) : le widget "superposition" ignore x/y/
width/height et pose lui-même position:fixed; inset:0; z-index:9999 sur
SA PROPRE balise (render_overlay.py) — mais le cadre .playElement/
.canvasElement qui l'entoure, PARTAGÉ PAR TOUS LES WIDGETS (filters/
element_style_filter.py), continuait quand même à poser
"position:absolute; z-index:<sa place dans le canevas>" (souvent petit,
ex. 1). Un élément positionné avec un z-index explicite crée un NOUVEAU
contexte d'empilement CSS : le 9999 posé plus profond ne se comparait
alors plus qu'AU SEIN de ce contexte, et perdait face au z-index (plus
grand) d'un élément ajouté APRÈS l'overlay sur le canevas — qui
s'affichait donc par-dessus le dialogue.

Correctif : _element_style ne pose plus aucune position/z-index pour ce
widget (position:static — sa place dans le flux est de toute façon
invisible, son contenu réel étant en position:fixed). Plus de contexte
d'empilement local créé à ce niveau : le z-index:9999 se compare
directement à tous les autres éléments de l'écran, et gagne toujours.

Profité de l'occasion pour donner à la boîte une largeur par défaut plus
raisonnable (render_overlay.py : max-width:min(560px, 90%) au lieu de
90% seul) — sur un écran de jeu large, "90%" donnait une boîte étirée
bord à bord peu lisible comme dialogue ; 560px reste confortable, et 90%
prend toujours le relais sur un écran étroit (mobile/portrait).

Nouveau test de régression (test_overlay_wrapper_does_not_trap_its_own_z_index) :
confirmé en échec sur l'ancien code (git stash), au vert avec le
correctif. 129 tests au vert au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 18:43:29 +02:00
williamandClaude Sonnet 5 28d8cd8cd0 Ajoute un script de démo pour tester dialogues/surbrillance/message différé
Les trois mécanismes de guidage du joueur discutés (boîte de dialogue en
popup, réaction à un clic, mise en évidence d'un élément à cliquer)
existent DÉJÀ dans le moteur (voir README, section "Survol, séquences
temporisées, surbrillance, overlay, verrouillage") : widget "Superposition
/ boîte de dialogue", action "Modifier un élément → Surbrillance", et
déclencheur "affichage" combiné à l'action "Attendre" pour un message qui
arrive tout seul après quelques secondes. Rien à coder côté moteur.

scripts/build_demo_dialogues.py construit, via les VRAIES routes Flask
(mêmes routes qu'utilise le navigateur), un jeu de démo persistant
("Demo Dialogues Surbrillance") avec deux écrans :
- "Tutoriel" : dialogue de bienvenue à l'ouverture de l'écran -> son
  bouton OK ferme le dialogue et met un autre bouton en surbrillance ->
  cliquer ce bouton l'éteint, le désactive (verrouillage anti-reclic) et
  ouvre un dialogue "Bravo" en réaction, fermable à son tour.
- "Message différé" : un dialogue "mentor" apparaît tout seul 3 secondes
  après l'affichage de l'écran (affichage -> attendre -> visibilité),
  sans aucune action du joueur.

Vérifié via /runtime-payload (nombre de nœuds/arêtes attendu sur les deux
écrans) et /play (overlay + surbrillance + attente bien exposés côté
rendu). 128 tests toujours au vert (script indépendant, aucun changement
moteur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 17:59:21 +02:00
williamandClaude Sonnet 5 169b720620 Corrige la sauvegarde des données d'un objet : collision d'id de formulaire entre objets
Bug rapporté : modifier une donnée dans l'onglet "Données" d'un objet
redirigeait vers le panneau d'un AUTRE objet (le premier de la liste) et
n'enregistrait rien sur le bon objet.

Cause : chaque objet a sa propre table SQLite (db/rows/insert_row.py), donc
les ids de ses lignes repartent de 1 - deux objets ont chacun une ligne
#1, #2, etc. Or game_dashboard.html générait les formulaires d'édition/
suppression d'une ligne avec un id DOM basé seulement sur r.id
("dataEditForm{{r.id}}"), jamais sur l'objet auquel elle appartient. Les
<input form="dataEditForm1"> de DEUX objets différents pointaient donc
vers le même id de formulaire dupliqué dans le document - et un id HTML
dupliqué se résout vers le PREMIER élément trouvé (le premier objet listé),
pas celui réellement affiché sous les yeux de l'utilisateur.

Correctif : les ids de formulaires ("dataEditForm"/"dataDeleteForm") et les
attributs form="..." des champs sont maintenant scopés par objet ET par
ligne ("dataEditForm{{d.id}}-{{r.id}}"), comme c'était déjà le cas pour les
panneaux (objectEditPanel, addEntryPanel...) via data-definition-id.

Nouveau test (tests/test_data_form_id_collision.py) : reproduit le
scénario exact (deux objets ayant chacun une ligne #1) et vérifie que les
ids de formulaire sont bien distincts et que la modification du second
objet ne touche pas le premier - confirmé en échec sur l'ancien code
(git stash) puis au vert avec le correctif. 128 tests au vert au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 17:31:58 +02:00
williamandClaude Sonnet 5 82a14fcb2b Panneau valeur variable Objet/Tableau : bouton +Ajouter visible et sauvegarde auto
Deux retours utilisateur sur le panneau "Configurer la valeur" ajouté au
commit précédent :
- le bouton "+ Ajouter" (style is-outlined) se voyait mal dans le panneau
  sombre -> repassé en bouton "primary" pleine largeur, et déplacé sous la
  liste des lignes (là où on l'attend pour ajouter une ligne de plus,
  plutôt qu'au-dessus, hors du flux de lecture) ;
- le bouton "OK - utiliser cette valeur" était un aller-retour inutile :
  supprimé. Chaque changement (texte tapé, clé, type, ajout ou retrait
  d'une ligne) réécrit désormais tout de suite le JSON dans le champ
  cible via saveVarValuePanel(), écoutée sur les événements input/change
  du conteneur des lignes - exactement comme si on tapait directement
  dans le champ, sans étape de validation séparée.

Aucun changement Python (127 tests toujours au vert) ; vérifié la syntaxe
du <script> rendu de l'onglet Variables via node --check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 14:56:03 +02:00
williamandClaude Sonnet 5 1b24df9546 Éditeur à champs pour les variables Objet/Tableau (plus de JSON à taper)
L'édition en JSON brut (textarea) n'était pas adaptée à un public non
développeur. Remplacée par un panneau "Configurer la valeur" (déplaçable/
redimensionnable/plein écran, comme les autres) avec une ligne par clé
(Objet) ou par élément (Tableau) : un champ Clé (Objet seulement), un
type (Texte/Nombre/Oui-Non) et une valeur en texte libre — le JSON est
reconstruit automatiquement au clic sur "OK", jamais tapé à la main.

Un seul panneau partagé (#varValuePanel) pour toutes les variables : à
l'ouverture (openVarValuePanel(targetInputId, mode)), reconstruit une
ligne par entrée déjà présente dans le JSON actuel de l'<input> ciblé —
un objet/tableau imbriqué en valeur est préservé tel quel (JSON.stringify
en texte opaque) plutôt que perdu, même si cette valeur précise n'est pas
éditable via une ligne (portée volontairement limitée aux valeurs
simples : suffisant pour une configuration ou une liste, sans la
complexité d'un éditeur JSON récursif).

- Barre "+ Créer une variable" : le champ de valeur devient en lecture
  seule pour Objet/Tableau, avec un bouton "🔧 Configurer" à côté.
- Ligne d'une variable existante : la cellule Valeur montre un aperçu en
  lecture seule + le même bouton "🔧 Configurer" (au lieu du textarea
  JSON du commit précédent).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 14:41:37 +02:00
williamandClaude Sonnet 5 aaf9954446 Variables globales Objet/Tableau, lisibles via un chemin dans les conditions et filtres
Deux nouveaux types de variable globale (db/constants.py) : "objet" et
"tableau" — valeur stockée en JSON (colonne TEXT existante), avec
validation à la création/modification (db/global_vars/
coerce_structured_value.py) : un JSON invalide retombe sur un défaut sûr
("{}"/"[]") plutôt que de corrompre silencieusement la variable pour
toutes ses lectures suivantes. apply_variable_action.py n'a besoin
d'aucun changement — "definir_texte" écrit déjà n'importe quelle chaîne
telle quelle.

Nouvelle résolution de chemin, partagée (screens/rendering/
filter_repeater_rows.py::_resolve_variable_path) : navigue dans la
valeur JSON d'une variable selon un chemin ".champ"/"[index]" chaînable
(ex. ".arme.degats", "[0].valeur") — ne lève jamais, renvoie None si le
JSON est invalide ou qu'un segment du chemin ne correspond à rien.
Branchée à deux endroits, qui lisaient déjà une variable globale :

- La valeur de comparaison {{$nom_variable}} (filtres de Répéteur ET
  Condition de visibilité, qui partagent le même
  _resolve_filter_value()) accepte maintenant un chemin optionnel :
  {{$perso.nom}}, {{$scores[0]}}. Le sélecteur "Variable globale" du
  panneau de propriétés (screen_edit.html, .filterValueVariable) gagne un
  champ "Chemin optionnel" à côté du choix de variable — même regex
  étendue côté JS (_VAR_REF_RE) que côté Python (_VAR_REF_PATTERN), pour
  que la valeur round-trip correctement à la réouverture du panneau.

- La variable VÉRIFIÉE par une Condition de visibilité en mode "variable"
  (choisie via un <select>, pas la syntaxe {{$...}}) gagne son propre
  nouveau contrôle "Chemin dans la variable" (visibility_condition_
  controls.py) — nécessaire pour comparer un champ d'un Objet ou un
  élément d'un Tableau, pas seulement la variable entière.

game_dashboard.html (onglet Variables) : la valeur par défaut de la barre
de création devient un <textarea> (fonctionne aussi bien pour un JSON
multi-ligne qu'un scalaire court), et la cellule "Valeur" du tableau des
variables existantes devient un <textarea> quand le type est objet/
tableau.

4 nouveaux tests (tests/test_variable_object_array.py) : validation JSON
à la création, filtre de Répéteur avec chemin chaîné, condition de
visibilité avec accès par index de tableau, chemin invalide/absent sans
plantage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 14:28:23 +02:00
williamandClaude Sonnet 5 b21f56e364 Filtre de colonnes + panneau de détail d'une entrée (onglet Données)
Un objet avec beaucoup de champs rendait le tableau "Données
enregistrées" illisible (toutes les colonnes serrées sur une ligne,
valeurs tronquées) — deux ajouts pour compenser :

1) "🔧 Colonnes" : menu déroulant (une case à cocher par champ, toutes
cochées par défaut) au-dessus du tableau — décocher un champ masque sa
colonne (th + td, via [data-col]) sans reconstruire le tableau. Un seul
menu ouvert à la fois, fermé au clic ailleurs sur la page.

2) 👁️ par ligne : ouvre un panneau de détail (déplaçable/redimensionnable/
plein écran par défaut, comme les autres) listant TOUS les champs de
cette entrée, un par ligne, valeur complète non tronquée — reprend le
style .rowDetailField/.rowDetailLabel/.rowDetailValue de l'ancienne
modale Bulma de data_list.html (retirée, mais ce CSS ne dépendait pas du
template et a été gardé). Un seul panneau PAR OBJET (pas par ligne) :
son contenu est reconstruit à l'ouverture en lisant en direct la ligne
du tableau déjà affiché (via [data-row-id] sur chaque <tr>) — reflète
donc aussi une modification tout juste saisie mais pas encore
"Enregistrer"ée, sans aller-retour serveur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 13:17:10 +02:00
williamandClaude Sonnet 5 943bf265de Panneaux flottants en plein écran par défaut, avec bouton pour en sortir
Chaque panneau (Nouvel objet, Modifier un objet, Ajouter un champ,
Ajouter une entrée) s'ouvre désormais en plein écran par défaut (marge
de 12px) plutôt qu'en petite fenêtre centrée — plus confortable dès
qu'il y a plusieurs champs/entrées à voir en même temps. Un bouton ⛶
dans l'en-tête bascule vers/depuis la taille et la position précédentes
(mémorisées le temps de la session, pas persistées), pour qui préfère
un panneau plus petit à côté du reste du tableau de bord.

makeFloatPanelFullscreenable(panel, toggleBtn) — générique, posée sur
panel._fsToggle — appelée par les 4 familles de panneaux existantes.
makeFloatPanelDraggable()/makeFloatPanelResizable() sortent d'abord
proprement du plein écran si l'utilisateur interagit manuellement
(glisser l'en-tête ou tirer le coin) : sans ça, bottom/right encore
actifs en plein écran auraient repris la main sur la position/taille
qu'on vient de poser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 13:06:58 +02:00
williamandClaude Sonnet 5 03db681022 Panneaux dédiés pour "Ajouter un champ"/"Ajouter une entrée"
La barre en ligne (tous les champs alignés horizontalement) devenait
illisible au-delà d'une poignée de champs — un objet à 10 champs
donnait une ligne de saisie qui débordait largement de l'écran.

Remplacée par un simple bouton "+ Ajouter" qui ouvre un panneau dédié
(déplaçable et redimensionnable, comme les autres) avec un formulaire
VERTICAL — un champ par ligne, étiquette au-dessus, comme un formulaire
normal — plutôt qu'entassé sur une seule ligne. Un panneau "Ajouter un
champ" et un panneau "Ajouter une entrée" par objet, tous deux cachés
par défaut.

Réutilise makeFloatPanelDraggable()/makeFloatPanelResizable() (déjà
génériques, voir commit précédent) — aucun nouveau mécanisme de
glisser-déposer à écrire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 12:58:16 +02:00
williamandClaude Sonnet 5 0e390a679b Ajoute l'onglet "Données" au panneau objet, retire object_view.html
Suite de la demande : le panneau "Modifier un objet" (crayon ✏️, onglet
Objets du tableau de bord) a maintenant 2 sous-onglets — "Champs" (déjà
en place) et "Données", qui reprend data_list.html + data_form.html
(retirés) : ajouter une entrée (une ligne compacte avec le bon type de
champ par colonne — texte/nombre/case à cocher/date/relation, comme
l'ancien formulaire), modifier/supprimer une entrée existante (tableau
dense, cellules éditables en ligne, même principe que "Champs
existants").

Sous-onglets scopés au panneau de LEUR objet (switchObjectPanelTab(),
classes .objectPanelTabs/.objectPanelTabPanel distinctes de .builderTabs/
.builderTabPanel) — plusieurs objets ont chacun leurs propres
sous-onglets indépendants sur la même page, sans jamais interférer avec
les onglets du tableau de bord lui-même.

routes/games/game_dashboard.py fournit maintenant, par objet : ses lignes
(rows_by_definition), les libellés lisibles de ses champs relation
(relation_labels_by_definition, pour l'affichage) et leurs options
(relation_options_by_definition, pour les <select>), ainsi que
referenced_by_definition (avertissement permanent si un autre objet a
une relation vers celui-ci — repris de l'ancien object_view.py, affiché
maintenant en continu plutôt qu'après une tentative de suppression
échouée).

data_form.py (partagé par data_new/data_edit), data_delete.py et
object_delete.py redirigent maintenant vers le tableau de bord
(?edit=<id>&subtab=data, +?blocked_row=<id> si la suppression d'une
entrée est bloquée par une relation) au lieu de object_view/object_edit.

Piège évité : de nombreux tests déduisent l'id d'un objet fraîchement
créé du DERNIER SEGMENT du chemin dans le header Location d'une
redirection (.../objects/<id>) — rediriger object_new directement vers
le tableau de bord (chemin sans id) cassait donc 33 tests d'un coup.
Fix : object_view.py reste en place, mais seulement comme redirecteur
(plus de page rendue) — object_new redirige toujours vers lui (chemin
qui se termine par l'id, donc les tests continuent de fonctionner), qui
redirige à son tour vers le tableau de bord.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 12:13:05 +02:00
williamandClaude Sonnet 5 b8dfdcb205 Corrige la nav pjax, panneau déplaçable/redimensionnable pour éditer un objet
1) Bug pjax trouvé : swapDocument() cherchait "header.topbar", qui n'a
jamais existé (c'est un <nav>, pas un <header>) — la barre de navigation
du jeu (ajoutée après pjax.js) n'était donc jamais mise à jour pendant
une navigation pjax : absente en arrivant sur un jeu sans Ctrl+F5, et
inversement laissée en place en revenant sur l'accueil où elle n'a rien
à faire. Fix : tout ce qui est hors <main> mais doit changer d'une page
à l'autre (topbar + barre du jeu) est regroupé dans un nouveau conteneur
stable #pageChrome, que pjax.js remplace en bloc — plus fiable qu'un
sélecteur qui ne correspondait à rien. CSS (flex:0 0 auto des layouts
plein-écran) mis à jour en conséquence.

2) Renommages demandés : onglet/panneau "Écrans du jeu" -> "Écrants",
"Éléments de jeu" -> "Templates" (tab, titre de panneau, bouton
"+ Créer un template", état vide, infobulle, confirmation de
suppression).

3) Le crayon ✏️ sur une ligne d'objet ouvre désormais un panneau
déplaçable ET redimensionnable (nouveau coin de redimensionnement
générique, .floatPanelResizeHandle) au lieu de naviguer vers
object_edit.html (retirée) — un panneau par objet, pré-rendu et
caché par défaut. Reprend telles quelles les fonctionnalités de
l'ancienne page : renommer l'objet, ajouter un champ (ligne compacte),
modifier/supprimer un champ existant (tableau dense déjà repris pour
"Nouvel objet"), supprimer l'objet. Les routes de champs (object_field_
add/edit/delete) et object_edit lui-même redirigent maintenant vers le
tableau de bord avec ?edit=<id>, pour rouvrir automatiquement le bon
panneau après l'action plutôt que de le fermer silencieusement.

makeFloatPanelDraggable()/makeFloatPanelResizable() généralisées pour
être partagées entre "Nouvel objet" et les panneaux d'édition, plutôt
que du code dupliqué par panneau.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 11:56:49 +02:00
williamandClaude Sonnet 5 e1216e99d5 Rend le panneau "Nouvel objet" compact — lignes de tableau, pas des cartes
Chaque champ prenait une grosse carte (~250px de haut : libellés Bulma
pleine taille, "Retirer ce champ" en texte) — inutilisable pour un objet
à 10+ champs, ce qui est pourtant l'usage visé par ce panneau.

Remplacé par une vraie ligne de tableau dense, sur le même principe que
"Champs existants" dans object_edit.html (déjà compact et sobre dans le
reste de l'outil) : une ligne = un champ, colonnes Nom/Type/Objet
lié/Mini/Maxi/Obligatoire, action "Retirer" réduite à une icône. Les
colonnes conditionnelles (Objet lié pour une relation, Mini/Maxi pour un
nombre) restent TOUJOURS présentes — sans quoi les colonnes de lignes
différentes ne s'aligneraient plus — seul leur contenu bascule entre le
vrai champ de saisie et un espace réservé "—", au lieu de masquer toute
la cellule comme avant.

object_form.js adapté en conséquence : bounds désormais 2 cibles
séparées (mini/maxi, chacune dans sa propre cellule) au lieu d'une seule
enveloppe commune, et chaque bascule s'accompagne de celle de son
espace réservé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 11:04:13 +02:00
williamandClaude Sonnet 5 d875557254 Fusionne les 4 pages de création dans le dashboard, retire le titre/BDD
Suite de la demande précédente : les 4 pages autrefois listées dans la
barre de navigation n'existent plus en tant que pages séparées — tout
vit désormais dans les onglets du tableau de bord (commit précédent) ou,
pour la création d'un objet, dans un panneau déplaçable.

- screens_list.html + sa route (screens_list) : supprimés (l'onglet
  "Écrans du jeu" du dashboard couvre déjà tout : créer, réordonner,
  éditer, supprimer). screen_new/screen_move/screen_delete redirigent
  maintenant vers le dashboard (tab=screens) au lieu de cette page.
- element_types.html : supprimé, mais la route element_types est
  conservée (GET redirige vers le dashboard, POST — utilisé par la barre
  de création repliable de l'onglet "Éléments de jeu" — continue de
  fonctionner). element_type_edit/element_type_delete redirigent aussi
  vers le dashboard.
- game_variables.html + sa route (game_variables) : supprimés (l'onglet
  "Variables" du dashboard couvre déjà tout). create_global_var/
  global_var_edit/global_var_delete redirigent vers le dashboard
  (tab=variables) au lieu de cette page.
- object_form.html : supprimé. La route object_new (POST) est conservée
  pour traiter la soumission du panneau — voir plus bas — mais ne rend
  plus de page pour un GET (redirige vers le dashboard).

Nouveau panneau déplaçable "Nouvel objet" sur le dashboard (bouton
"+ Nouvel objet" de l'onglet Objets) : réutilise .floatPanel/
.floatPanelHeader/.floatPanelBody (déjà utilisées dans l'éditeur d'écran)
avec une nouvelle variante centrée (.floatPanel--center) et son propre
glisser-déposer minimal (pas de position persistée, contrairement aux
panneaux de l'éditeur d'écran — inutile pour un panneau ouvert
ponctuellement). Contenu et script (object_form.js) repris tels quels de
l'ancienne page.

base.html : la barre de navigation du jeu n'a donc plus que 2 liens —
"📊 Tableau de bord" (nouveau) et "▶️ Jouer" (toujours en dernier).

game_dashboard.html : titre du jeu et chemin de la base de données
retirés (redondants avec le nom déjà visible dans l'onglet du
navigateur/la barre de nav).

Les liens "crée-en un"/"gérer les variables" dans l'éditeur d'écran
(screen_edit.html) pointent maintenant vers le dashboard avec le bon
onglet (?tab=...), lu et appliqué au chargement de la page
(switchDashTab() côté JS).

2 tests (test_screens_and_elements.py) mis à jour : ils vérifiaient le
contenu des pages supprimées (element-types, screens) — adaptés pour
vérifier la même chose sur le dashboard, qui porte maintenant cette
information.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 10:54:19 +02:00
williamandClaude Sonnet 5 65450a5719 Refait le dashboard en fenêtre à onglets avec de vrais tableaux
Le précédent dashboard (grille de cartes compactes) ne correspondait pas
à ce que l'utilisateur voulait : une seule fenêtre avec de vrais tableaux
de données (denses, colonnes nettes), un bouton "Créer" par catégorie
dans l'en-tête, et une navigation horizontale pour passer d'une catégorie
à l'autre.

Réutilise telles quelles .builderTabs/.builderTabBtn/.builderTabPanel
(déjà utilisées pour "Écran / Logique / Timeline" dans l'éditeur d'écran)
plutôt que d'inventer un 2e système d'onglets — même sensation partout
dans l'outil. Un onglet par catégorie (Écrans/Objets/Éléments de
jeu/Variables), chacun avec :
  - un bouton "+ Créer" dans l'en-tête qui révèle une barre de création
    compacte (repliée par défaut) — sauf pour les Objets, dont la
    création (plusieurs champs typés) reste sur sa propre page dédiée,
    trop complexe pour tenir dans une barre ;
  - le VRAI tableau de gestion de cette catégorie (colonnes, actions),
    repris tel quel de screens_list.html/element_types.html/
    game_variables.html plutôt que réinventé en version appauvrie.

La page défile désormais normalement (retrait de body.objectEditBody/
content-objectEdit, pensés pour une hauteur figée avec défilement
interne) — une liste peut être longue, pas besoin d'un défilement séparé
par panneau ici.

routes/games/game_dashboard.py fournit en plus variable_types
(db.GLOBAL_VARIABLE_TYPES) pour la barre de création de variable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 10:15:37 +02:00
williamandClaude Sonnet 5 8f45169209 Retire le fil d'Ariane, centre/réordonne la barre de nav, refait le dashboard
Trois demandes distinctes de l'utilisateur, regroupées car elles touchent
toutes à la navigation d'un jeu :

1. Fil d'Ariane retiré (devenu redondant avec la barre de navigation du
   jeu ajoutée au commit précédent) : bloc breadcrumb_wrap retiré de
   base.html, et son override ({% block breadcrumb %}) retiré des 9
   templates qui le définissaient encore. .breadcrumbBar (CSS) retiré,
   y compris des règles flex:0 0 auto de body.objectEditBody/builderBody.

2. Liens de .gameNavBar centrés (justify-content:center).

3. "Jouer" déplacé en dernier lien (c'est une action à part — ouvre
   l'aperçu jouable dans un nouvel onglet — pas un éditeur de plus comme
   les 4 autres).

4. game_dashboard.html devient un vrai tableau de bord : une grille de
   cartes (Écrans/Objets/Éléments de jeu/Variables), chacune listant les
   entrées existantes avec un accès direct (clic = éditeur concerné) et
   un lien "Gérer" vers la page dédiée pour créer/réorganiser. Remplace
   l'ancien panneau "Créer" (redondant avec la barre de navigation
   persistante) et la simple table "Objets définis". routes/games/
   game_dashboard.py alimente maintenant aussi screen_list, element_types
   (+ usage) et variables, en réutilisant list_screens/
   list_element_types/element_type_usage_count/list_global_variables déjà
   utilisés ailleurs.

Vérifié en rendant toutes les pages concernées via le client de test
Flask : barre de nav présente partout où un `game` est dans le contexte
(absente sur l'accueil), fil d'Ariane absent partout, ordre des liens
avec "Jouer" en dernier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 10:04:09 +02:00
williamandClaude Sonnet 5 de89115534 Ajoute une barre de navigation persistante entre les éditeurs d'un jeu
Jusqu'ici, les liens vers les différents éditeurs d'un jeu (Écrans,
Éléments de jeu, Variables, Jouer, Objet) n'existaient que dans le
panneau "Créer" du tableau de bord (game_dashboard.html) — changer
d'éditeur obligeait à revenir sur cette page à chaque fois.

base.html expose désormais une 2e barre (.gameNavBar), sous la barre
"Forge Engine" et au-dessus du fil d'Ariane, reprenant ces mêmes 5
liens — visible sur TOUTE page qui met un `game` dans le contexte du
template (déjà fait par chaque route pour le fil d'Ariane, donc aucun
changement de route nécessaire), absente sur l'accueil (liste des jeux,
pas de jeu courant). L'onglet correspondant à la section actuelle est
mis en évidence via un simple préfixe sur request.path.

static/style.css : nouvelle barre en ligne (contrairement à .navList/
.navRow, empilés verticalement dans le panneau "Créer" du tableau de
bord, réutilisés tels quels ailleurs). Ajoutée aux règles flex:0 0 auto
de body.objectEditBody/body.builderBody (mise en page plein-écran des
éditeurs) aux côtés de .topbar/.breadcrumbBar, sans quoi elle aurait
cassé la répartition de hauteur figée de ces pages.

Vérifié en rendant plusieurs routes via le client de test Flask : barre
absente sur l'accueil, présente partout ailleurs (tableau de bord, liste
des écrans, éditeur d'écran normal ET d'écran-modèle, éléments de jeu,
variables, nouvel objet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 09:20:05 +02:00
williamandClaude Sonnet 5 ffedb16d8a Inclut les écrans-modèles dans gameData.flows/animations
Cause du "la logique posée sur le modèle ne s'applique pas dans la
scène" (persistant malgré les 2 commits précédents) : full_game_payload()
appelait list_screens(slug) SANS include_templates=True — un
écran-modèle (ex. "Modèle : mail content") n'apparaissait donc jamais
dans gameData.flows ni gameData.animations côté client. findTriggerNode()
cherche pourtant bien un déclencheur dans TOUTES les clés de
gameData.flows — mais si l'écran-modèle n'y a même pas d'entrée, il n'y
a rien à trouver, quelle que soit la justesse de cette recherche.

Fix : les nœuds/fils de logique et les clips d'animation sont désormais
lus pour TOUS les écrans (list_screens(slug, include_templates=True)),
dans une boucle séparée de celle qui construit payload_screens — celle-
ci continue de ne lister que les vrais écrans, pour ne jamais rendre un
écran-modèle comme un <div class="playScreen"> à part entière (il n'est
jamais affiché tel quel, seulement rechargé en direct à l'intérieur d'un
élément qui l'utilise).

Vérifié sur les vraies données du jeu de test : gameData.flows contient
désormais bien l'écran 3 (le modèle "mail content"), avec son
déclencheur "Au survol" et son action "Rendre visible" ; payload_screens
ne contient toujours que l'écran réel (1).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 08:42:20 +02:00
williamandClaude Sonnet 5 e653f95d37 Ouvre les onglets Logique/Animation dans l'éditeur d'un modèle réutilisable
Ces onglets ("Logique de la scène", "Timeline d'animation") étaient
masqués sur l'écran-modèle d'un élément de jeu réutilisable, avec un
commentaire expliquant pourquoi : à l'époque, ses enfants étaient COPIÉS
en base avec un NOUVEL id à chaque exemplaire posé
(instantiate_template_tree) — une logique/animation posée dans le
modèle référencerait donc des ids qui n'existent plus une fois
l'élément utilisé ailleurs.

Ce mécanisme a depuis été retiré (voir le commentaire dans
list_elements.py) : le contenu d'un élément de jeu réutilisable est
désormais TOUJOURS rechargé EN DIRECT depuis son écran-modèle à chaque
affichage, avec les MÊMES ids à chaque exemplaire. Combiné au commit
précédent (findTriggerNode/runFlowFrom/collectAnimationClips côté
play.html, qui exécutent maintenant correctement un déclencheur/une
animation posé dans un modèle, où qu'il soit utilisé), la restriction
de cette page n'avait donc plus lieu d'être — elle bloquait justement la
fonctionnalité que le commit précédent venait de rendre possible.

Les données nécessaires (flow_nodes/flow_edges/elements du modèle,
etc.) étaient déjà calculées sans condition par la route
(routes/screens/screen_edit.py) ; seul le template masquait les deux
onglets et leur contenu derrière {% if not screen.is_template %}.
switchBuilderTab() détecte déjà la présence des panneaux via
HAS_FLOW_PANEL/HAS_ANIM_PANEL (document.getElementById), donc aucun
changement JS n'était nécessaire.

Vérifié en rendant réellement /game/test/screens/3/edit (l'écran-modèle
"mail content") via le client de test Flask : les deux onglets sont
maintenant bien présents, et l'écran normal (id=1) n'est pas affecté.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 07:41:48 +02:00
williamandClaude Sonnet 5 17c5e93d8d Fait fonctionner logique et animations d'un modèle réutilisable partout où il est posé
Jusqu'ici, la logique (déclencheurs Au clic/Au survol/Fin du survol) et
les animations posées dans l'éditeur de l'écran-MODÈLE d'un élément de
jeu réutilisable (ex. "mail content") sur SES PROPRES enfants ne
s'exécutaient jamais quand cet élément était simplement posé sur une
autre scène : findTriggerNode() cherchait bien le déclencheur dans tous
les écrans (modèles compris) mais runFlowFrom() n'exécutait ensuite le
graphe que dans l'écran RÉELLEMENT affiché — le nœud trouvé n'existait
pas dans ce graphe-là, donc rien ne se déclenchait, silencieusement.
Même limitation pour les animations, dont la timeline ne lisait que les
clips propres à l'écran affiché.

Logique (templates/play.html) :
- findTriggerNode() renvoie désormais { node, screenId } plutôt que
  juste le nœud, pour transmettre l'écran D'ORIGINE du déclencheur (qui
  peut être un écran-modèle).
- runFlowFrom(nodeId, flowScreenId) accepte un 2e paramètre optionnel
  (par défaut l'écran affiché, comportement inchangé pour tout le
  reste) pour exécuter le graphe dans le BON écran.
- bindClicks()/bindHoverTriggers() passent maintenant cet écran
  d'origine à runFlowFrom(). runScreenShowTriggers() (déclencheur "À
  l'affichage de l'écran") reste volontairement inchangé — hors scope,
  ambiguïté sur plusieurs exemplaires d'un même modèle sur un écran.

Animations (screens/payload/full_game_payload.py, templates/play.html) :
- Le payload expose désormais element_types (element_type_id -> id de
  son écran-modèle), via screens.list_element_types() déjà existant.
- collectAnimationClips(screenId) rassemble récursivement les clips de
  l'écran affiché ET de tout écran-modèle utilisé par un de ses
  éléments (garde anti-boucle, dédoublonnage par écran).
- applyAnimationClip() cible désormais TOUS les exemplaires d'un id
  d'élément (querySelectorAll, plus querySelector) : un enfant de
  modèle garde le même id à chaque exemplaire, y compris pour chaque
  ligne d'un Répéteur utilisant ce modèle comme gabarit de ligne.

Limite connue, non corrigée ici (pas la demande) : une action "Modifier
un élément" ciblant un enfant de modèle reste, elle, scopée au premier
exemplaire trouvé dans le DOM (document.querySelector singulier dans
runActionNode/applyElementProperty) — sans impact pour un modèle posé
une seule fois par écran, comme dans le cas d'usage actuel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 07:33:25 +02:00
williamandClaude Sonnet 5 19d3810164 Calcule rendered_html pour CHAQUE élément, pas seulement le premier niveau
Cause racine réelle des 2 précédents correctifs (commits 20adfa4,
f4a7a73, 5e4e226, f016a81) qui n'avaient AUCUN effet visible malgré des
redémarrages en règle : list_elements() ne calculait "rendered_html" que
pour les éléments de PREMIER NIVEAU (parent_id NULL). Le nouveau ciblage
côté JS (play.html) cherche pourtant à repérer, pour un élément imbriqué
comme le Répéteur ou "mail content", s'il porte lui-même un marqueur ou
un {{champ}} non résolu — mais côté serveur, ces éléments n'avaient tout
simplement PAS de rendered_html du tout : `e.rendered_html` valait
`undefined`, donc `hasMarker`/`hasUnresolvedPlaceholder` retombaient
toujours à `false` pour eux, laissant SEUL le conteneur racine de
premier niveau comme candidat — reproduisant exactement le bug d'origine
(tout l'écran régénéré à chaque clic) qu'aucun des correctifs côté JS ne
pouvait donc jamais résoudre, quelle que soit la justesse de leur
logique de filtrage.

Fix : chaque élément (imbriqué ou non) reçoit désormais son propre
rendered_html — un élément imbriqué s'y retrouve deux fois (une fois
dans le rendered_html de son ancêtre de premier niveau, utilisé pour le
rendu HTML initial de la page ; une fois dans le sien propre, utilisé
par le ciblage précis côté JS). Vérifié par simulation directe sur le
payload réel du jeu de test : les cibles calculées sont maintenant
exactement les 3 jauges, le Répéteur et "mail content" — plus jamais le
conteneur racine de l'écran.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 06:49:41 +02:00
williamandClaude Sonnet 5 f016a81dc6 Résout {{champ}} en place au lieu de recréer le nœud (zéro flash, façon React)
L'utilisateur a raison de pointer que le vrai souci n'était pas un bug
isolé mais l'APPROCHE elle-même : detruire puis reconstruire un nœud du
DOM à chaque clic (même bien ciblé, comme depuis les 2 derniers commits)
cause toujours un flash visuel, puisque tout état transitoire du
sous-arbre (visibilité posée par "Modifier un élément", focus...) est
perdu et reconstruit à neuf. C'est ce qui donnait l'impression trompeuse
d'un "rechargement" — un comportement JS parfaitement normal quand on
manipule le DOM ainsi, mais évitable : c'est exactement le problème que
la réconciliation ciblée de React (ne patcher que ce qui a changé,
jamais recréer un nœud pour rien) résout côté framework.

applyOpenRowBindings() ne remplace donc plus JAMAIS le nœud de l'élément
ciblé (ex. "mail content") — il patche directement, en place :
  - un nœud TEXTE contenant {{champ}} est coupé en 3 (texte avant, un
    <span data-bind-field="champ">, texte après) LA PREMIÈRE FOIS
    SEULEMENT ; toute ouverture suivante se contente de changer le
    textContent de ce span — plus aucune reconstruction ensuite.
  - un ATTRIBUT contenant {{champ}} (ex. href="{{link_real_url}}") voit
    son gabarit d'origine mémorisé sur data-bind-attr-<nom> au premier
    passage, pour être recalculé et réécrit directement à chaque fois
    sans jamais reconstruire le nœud.

Plus aucun nœud n'étant détruit, la sauvegarde/restauration de l'état
visuel transitoire (ajoutée dans un commit précédent pour compenser
cette destruction) devient inutile et est retirée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 06:24:42 +02:00
williamandClaude Sonnet 5 5e4e226794 Corrige le filtre "élément le plus spécifique" (logique inversée)
Les deux commits précédents (ciblage des éléments imbriqués dans
applyOpenRowBindings() et refreshRuntimeData()) n'avaient AUCUN effet
visible, confirmé par l'utilisateur après redémarrage du serveur — cause
trouvée : leur filtre "ne garder que les éléments les plus spécifiques"
vérifiait l'inverse de ce qu'il fallait.

Un CONTENEUR contient toujours le HTML de ses descendants dans son
propre rendered_html — donc un ancêtre "a le marqueur/placeholder" quasi
systématiquement dès qu'un descendant l'a. Le filtre précédent excluait
un élément candidat si un de ses ANCÊTRES était candidat — ce qui, vu ce
qui précède, ne gardait quasiment jamais que l'ancêtre RACINE de
l'écran, reproduisant exactement le bug d'origine (tout l'écran
régénéré) que ces commits visaient à corriger.

Fix : inversion du sens du filtre — un candidat est désormais exclu si
l'un de ses PROPRES DESCENDANTS est aussi candidat (le descendant sera
déjà régénéré individuellement, inutile de régénérer aussi son
ancêtre). Vérifié par une simulation Node.js reproduisant la structure
réelle de l'écran de test (Répéteur niché sous 2 conteneurs, "mail
content" sous 2 autres) : la nouvelle logique cible bien uniquement le
Répéteur et "mail content", plus jamais le conteneur racine de l'écran.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 13:04:01 +02:00
williamandClaude Sonnet 5 f4a7a7340e Cible aussi les éléments imbriqués dans refreshRuntimeData()
Même défaut que celui corrigé dans applyOpenRowBindings() (commit
précédent), mais dans le second endroit qui régénère l'écran après un
changement de donnée : refreshRuntimeData() ne pouvait régénérer que les
éléments de PREMIER NIVEAU (seuls eux ont un data-el-id sur leur wrapper
.playElement). Un Répéteur niché dans un conteneur — comme celui de cet
écran — n'est jamais du premier niveau : c'est donc son ANCÊTRE de
premier niveau qui portait le marqueur "repeaterItem" à l'intérieur et
se faisait régénérer en entier à sa place, potentiellement l'écran
complet (jauges, onglets compris) si l'écran n'a qu'un seul gros
conteneur racine. C'était la cause réelle du "rechargement" toujours
visible après le précédent correctif : celui-ci ne portait que sur
applyOpenRowBindings(), pas sur cette 2e régénération déclenchée par
"Modifier une donnée"/"Modifier une variable".

Fix : même principe que le commit précédent — cible chaque élément
marqué (repeaterItem/jaugeBar/visibilityGated) directement via son
data-element-id, à n'importe quel niveau d'imbrication, en ne gardant
que les plus "hauts" parmi les éléments marqués pour ne jamais régénérer
un même nœud deux fois.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 12:51:57 +02:00
williamandClaude Sonnet 5 20adfa4169 Ne régénère plus que l'élément concerné par "Ouvrir la ligne cliquée"
Cause du "rechargement" perçu par l'utilisateur (et du flash à vide sur
la ligne de Répéteur juste cliquée, visible sur une vidéo de repro) :
applyOpenRowBindings() ne pouvait cibler que les éléments de PREMIER
NIVEAU de l'écran (seuls eux ont un wrapper .playElement dans le DOM).
Sur cet écran, "mail content" est niché à 2 conteneurs de profondeur, et
le SEUL élément de premier niveau est le conteneur racine de tout
l'écran — donc chaque clic sur une ligne de Répéteur régénérait
littéralement tout l'écran (jauges, onglets, Répéteur compris) pour ne
mettre à jour qu'un seul panneau de détail, avec un flash à vide pendant
la reconstruction.

Fix : applyOpenRowBindings() cible maintenant directement, à n'importe
quel niveau d'imbrication, le(s) élément(s) qui portent réellement un
{{champ}} non résolu (repéré via document.querySelector
('[data-element-id=...]'), disponible sur CHAQUE élément rendu, pas
seulement les élément de premier niveau) — et seulement les plus "hauts"
parmi eux, pour ne jamais régénérer un même nœud deux fois. Seul "mail
content" est donc désormais remplacé (via replaceWith), sans toucher au
Répéteur ni au reste de l'écran. La sauvegarde/restauration de l'état
visuel transitoire (style, classes, dataset hors clickBound/hoverBound/
hoverTriggerBound) suit le même principe, appliquée au nœud remplacé et
à ses descendants.

Aucun aller-retour réseau n'a jamais eu lieu ici (refreshRuntimeData()
utilise déjà fetch/JSON, pas de navigation de page) — la sensation de
rechargement venait uniquement de la granularité du remplacement DOM,
pas d'un manque d'AJAX.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 09:02:49 +02:00
williamandClaude Sonnet 5 5114372cc4 Réattache les gestionnaires de clic après "Ouvrir la ligne cliquée"
Root cause enfin identifiée grâce à un log console instrumenté par
l'utilisateur (indispensable — sans lui les précédents correctifs
visaient le mauvais chemin de code, refreshRuntimeData(), qui ne se
déclenchait même pas dans ce scénario) : l'action "Ouvrir la ligne
cliquée" (ouvrir_ligne) appelle applyOpenRowBindings() DIRECTEMENT,
sans jamais rappeler bindClicks() ensuite — contrairement à
refreshRuntimeData(), qui elle le fait déjà correctement.

Si l'écran a un Répéteur ET un panneau de détail (avec des {{champ}})
posés dans un même conteneur parent, applyOpenRowBindings() régénère
tout ce sous-arbre — Répéteur compris — pour résoudre les {{champ}} du
panneau. Les lignes du Répéteur héritent alors de nœuds DOM tout neufs,
sans le moindre écouteur de clic (le garde-fou anti-doublon de
bindClicks() repose sur dataset.clickBound, absent sur un nœud neuf,
mais bindClicks() lui-même n'était jamais rappelé pour les attacher).

Symptôme exact reproduit : le tout premier clic sur une ligne fonctionne
(gestionnaires posés au chargement de la page), plus AUCUN clic ne
répond ensuite sur AUCUNE ligne, sans erreur console — confirmé par un
log montrant runFlowFrom() jamais réinvoqué au clic suivant, et
manuellement réparé en rappelant bindClicks() à la main dans la
console.

Fix : bindClicks()/bindHoverTexts()/bindHoverTriggers() sont maintenant
rappelés juste après applyOpenRowBindings() dans le gestionnaire de
"Ouvrir la ligne cliquée", comme ils le sont déjà dans
refreshRuntimeData().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 08:18:43 +02:00
williamandClaude Sonnet 5 ca7daf9a31 Réattache toujours les gestionnaires de clic après un rafraîchissement de données
Test de diagnostic déterminant : après le blocage rapporté (plus aucune
ligne de Répéteur ne répond après le tout premier clic), appeler
manuellement bindClicks() dans la console suffisait à tout réparer —
donc ni le DOM ni le graphe de logique n'étaient en cause, seule
l'INVOCATION de bindClicks() manquait à un moment donné.

Cause : refreshRuntimeData() faisait un retour anticipé silencieux
(`if (!screenData || !screenDiv) return;`) qui sautait, avec lui,
TOUT le reste de la fonction — y compris bindClicks(), bindHoverTexts()
et bindHoverTriggers() — sans le moindre message d'erreur, laissant les
éléments régénérés (Répéteur compris) sans aucun écouteur pour le reste
de la partie.

Fix : ce garde-fou ne protège plus désormais que le bloc de régénération
du contenu de l'écran (qui a effectivement besoin de screenData/
screenDiv) ; les réattachements, eux, s'exécutent toujours ensuite, quoi
qu'il arrive. Un try/catch autour de la régénération ajoute en prime un
filet de sécurité : toute erreur inattendue s'y loggera clairement au
lieu de bloquer silencieusement le reste, si jamais ce n'était pas
l'unique cause.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 07:47:19 +02:00
williamandClaude Sonnet 5 024da11934 Corrige le blocage des clics après le premier rafraîchissement de données
Régression introduite par le commit précédent (préservation de l'état
visuel transitoire dans applyOpenRowBindings) : la restauration du
dataset complet d'un nœud copiait aussi clickBound/hoverBound/
hoverTriggerBound — des indicateurs INTERNES au moteur (voir bindClicks/
bindHoverTexts/bindHoverTriggers), jamais un état posé par une action
"Modifier un élément". Un nœud tout juste régénéré se retrouvait donc
marqué "déjà lié" à tort, alors qu'aucun écouteur de clic n'y était
réellement rattaché : bindClicks() le voyait déjà "bound" et sautait
son rattachement, rendant l'élément silencieusement inerte pour le
reste de la partie.

Symptôme rapporté : dans un écran avec un Répéteur ET un panneau de
détail utilisant des {{champ}}, le premier clic sur une ligne fonctionne
(exécuté par les gestionnaires posés au chargement de la page), mais
plus aucun clic ne répond ensuite sur AUCUNE ligne — le Répéteur étant
regénéré dans le même sous-arbre que le panneau de détail (ancêtre
commun avec des {{champ}} non résolus), donc concerné par la même
restauration de dataset.

Fix : exclure ces trois clés internes de la sauvegarde/restauration —
seul l'état réellement transitoire (style inline, classes, data-toggle-*
posés par "Modifier un élément") doit survivre à la regénération.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 07:31:40 +02:00
williamandClaude Sonnet 5 438e561b45 Préserver l'état visuel transitoire lors du rafraîchissement des données
Cause réelle du bug rapporté ("clic sur une ligne = comme un rechargement,
impossible de cliquer sur une deuxième ligne") : applyOpenRowBindings()
regénère entièrement le sous-arbre d'un élément contenant des {{champ}}
(ex. "mail content") à partir de son HTML D'ORIGINE, tel que rendu par le
serveur — donc avec son style PAR DÉFAUT (ici "invisible" via le réglage
Disposition > Visibilité). Cette fonction est appelée après CHAQUE
rafraîchissement de données (refreshRuntimeData), y compris pour un
changement de donnée sans rapport avec ce panneau.

Or une action "Modifier un élément → Rendre visible" ne modifie JAMAIS la
base : c'est un changement DOM transitoire (style.visibility = ''). Quand
un clic sur une ligne de Répéteur déclenche EN PARALLÈLE "Ouvrir la ligne
cliquée" + "Rendre visible" + "Modifier une donnée", la branche
"Modifier une donnée" est asynchrone (aller-retour serveur) et termine
après les deux autres, synchrones. Son refreshRuntimeData() qui suit
regénère alors "mail content" depuis son état par défaut, écrasant le
"Rendre visible" qui venait tout juste d'être posé — le panneau redevient
invisible. Un second clic sur le MÊME mail "corrige" l'affichage car la
donnée est déjà à jour, donc la Condition ne redéclenche plus l'action de
modification, plus de refresh, plus d'écrasement ; mais ouvrir un AUTRE
mail reproduisait le même écrasement.

Fix : avant de remplacer wrapper.innerHTML, sauvegarder le style inline,
la classe et les data-* de chaque élément du sous-arbre, puis les
réappliquer juste après la regénération — la résolution des {{champ}}
reste correcte (c'est le but premier de la fonction) sans plus annuler
les changements posés par une action "Modifier un élément" au même clic.

Les trois actions du graphe (ouvrir la ligne, rendre visible, modifier la
donnée) restent connectées telles quelles, sans aucun retrait.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 18:17:35 +02:00
williamandClaude Sonnet 5 96d017c21f Évite de rejouer les déclencheurs "affichage"/l'animation quand on est déjà sur l'écran ciblé
Bug remonté : cliquer sur une ligne de Répéteur "semblait recharger la
page", et devenait impossible à recliquer une deuxième fois - alors que
le graphe de logique voulu (modifier la donnée + rendre visible un détail
+ "Ouvrir la ligne cliquée") reste sur le MÊME écran que le Répéteur.

Cause : showScreen() rejouait INCONDITIONNELLEMENT les déclencheurs "À
l'affichage de l'écran" et relançait la timeline d'animation depuis le
début à chaque appel - même quand l'écran cible est déjà celui affiché
(le cas normal pour "Ouvrir la ligne cliquée" combinée à une action
"Modifier un élément → Visibilité" sur le même clic, pensées pour
fonctionner ensemble SUR le même écran qu'un Répéteur). Ça rejouait donc
les animations d'entrée et pouvait faire repasser la visibilité à son
état initial via un déclencheur "affichage", entrant en conflit avec
l'action "Rendre visible" du même clic - d'où l'impression de
rechargement, et le blocage : reflow/re-rendu qui se disputent avec
l'état attendu.

Fix : showScreen() ne fait plus rien du tout si l'écran ciblé est déjà
celui affiché - aucun changement visuel à faire, donc aucune raison de
rejouer son "premier affichage". Changer vers un écran DIFFÉRENT continue
de tout rejouer normalement. Les trois actions du graphe restent
déclenchées à chaque clic, sans plus se marcher dessus.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 17:47:25 +02:00
williamandClaude Sonnet 5 b094342097 Ajoute "Ligne cliquée (Répéteur)" comme cible pour une Condition/action de la Logique de la scène
Problème remonté : un déclencheur "Au clic" posé sur un Répéteur exécute
le MÊME graphe pour n'importe quelle ligne cliquée - or une Condition
("Si is_opened est égal à Non") ou une action "Modifier une donnée" ne
pouvaient viser qu'une ligne FIXE, choisie à la création du nœud dans
l'éditeur. Impossible donc de dire "modifie le champ DE LA LIGNE QUE JE
VIENS DE CLIQUER", puisque cette ligne n'est justement jamais connue à
l'avance.

Nouvelle valeur sentinelle CLICKED_ROW_ID (-1, screens/flow/constants.py,
ne collisionne jamais avec un vrai id de ligne) proposée en tête de TOUTE
liste déroulante "Ligne concernée" (clause principale et clauses
supplémentaires d'un nœud Condition, cible d'une action "Modifier une
donnée") : "🖱️ Ligne cliquée (Répéteur)".

Résolution au moment de l'exécution, pas à la création du nœud :
- Condition (évaluée côté client) : readFieldValue() (play.html) résout
  -1 en window.lastClickedRowId, déjà capturé par bindClicks() au clic sur
  une ligne de Répéteur (déjà utilisé par "Ouvrir la ligne cliquée").
- Action "Modifier une donnée" (exécutée côté serveur) : le client envoie
  clicked_row_id dans le corps de la requête POST ; flow_node_run_data.py
  ne s'en sert que si le nœud vise justement CLICKED_ROW_ID, sinon la
  ligne fixe stockée sur le nœud reste utilisée normalement.

Ajoute tests/test_flow_clicked_row.py (ligne cliquée seule modifiée,
absence de clic = no-op plutôt que plantage, non-régression d'une cible
fixe, présence de l'option dans l'éditeur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 17:19:41 +02:00
williamandClaude Sonnet 5 7d48445443 Ajoute "Variable globale" au sélecteur "Valeur fixe / Donnée d'un autre objet"
Le sélecteur de valeur de comparaison (tout contrôle "..._valeur" : filtre
du Répéteur, Donnée liée, condition de visibilité) proposait déjà une
valeur fixe ou le champ d'un AUTRE objet - manquait la possibilité de
comparer à une variable globale (voir db/global_vars/), qui change elle
aussi en cours de partie mais n'est rattachée à aucun objet précis.

Nouvelle syntaxe interne "{{$nom_variable}}" (le "$" la distingue sans
ambiguïté de "{{Objet.champ}}", qui attend toujours un point) :
_resolve_filter_value (filter_repeater_rows.py) va lire sa valeur actuelle
via db.get_global_variable, comme "{{Objet.champ}}" le fait déjà pour un
champ d'objet. Le panneau de propriétés gagne un troisième mode
"Variable globale" à côté de "Valeur fixe"/"Donnée d'un autre objet",
avec la liste déroulante des variables existantes.

Corrige au passage un bug latent découvert en testant bout en bout : un
Répéteur SANS modèle de ligne (texte brut avec {{champ}}) plantait en
mode jouable avec TypeError - render_repeater.py substitue lui aussi
directement les {{champ}} du ctx dans ce cas (repli), et ce ctx porte
aussi _forge_play_mode (un booléen, voir render_element_html.py) depuis
l'ajout de la condition de visibilité - déjà corrigé pour le chemin
générique (apply_ctx.py) mais pas pour ce chemin séparé.

Le sélecteur de champ pour insérer {{champ}} dans "Contenu" (demandé dans
le même message) existe déjà depuis un tour précédent (voir
insertFieldAtCursor()) - vérifié toujours fonctionnel.

Ajoute tests/test_filter_value_global_variable.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:51:18 +02:00
williamandClaude Sonnet 5 6894c5fc95 Remplace le confirm() natif par une modale custom qui avertit des suppressions en cascade
Depuis le dernier correctif, supprimer un élément supprime aussi en
silence tout nœud de la Logique de la scène qui le référence (déclencheur/
action, voir delete_element.py) - nécessaire pour éviter le plantage
"FOREIGN KEY constraint failed", mais l'utilisateur n'était jamais prévenu
qu'un bout de sa logique disparaissait en même temps.

Nouvelle route GET .../elements/<id>/delete-impact (element_delete_
impact.py) : calcule, sans rien supprimer, combien de nœuds de la Logique
de la scène référencent cet élément OU l'un de ses descendants (partage
element_descendant_ids.py avec delete_element.py, pour rester exactement
cohérent avec ce qui sera réellement supprimé).

Les deux boutons "supprimer" de screen_edit.html (élément sélectionné, et
onglet d'un widget Onglets) ouvrent maintenant une modale custom
(deleteConfirmModal, même famille que le sélecteur d'icônes) au lieu du
confirm() natif du navigateur : elle interroge cette route juste après
ouverture et affiche un avertissement dédié si le nombre remonté est non
nul, avant que l'utilisateur ne confirme quoi que ce soit - impossible à
faire avec confirm(), dont le texte est figé au moment du rendu de la
page. La confirmation soumet ensuite le formulaire normalement (via
requestSubmit(), intercepté par pjax.js comme n'importe quel autre
formulaire).

Ajoute deux tests pour la nouvelle route (impact nul, impact non nul sans
rien supprimer).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:27:34 +02:00
williamandClaude Sonnet 5 1e86077e45 Corrige un plantage à la suppression d'un élément référencé par la Logique de la scène
Bug remonté : supprimer un élément plantait avec sqlite3.IntegrityError:
FOREIGN KEY constraint failed.

Cause : trigger_element_id/target_element_id (_flow_nodes, voir
ensure_flow_schema.py - un nœud "clic sur cet élément" ou "Modifier cet
élément"/"Activer cet onglet") et target_element_id (l'ancien système
_actions, conservé pour compatibilité) référencent _screen_elements(id)
SANS ON DELETE CASCADE - volontairement, un élément ne doit pas pouvoir
disparaître "par erreur" en cascade depuis un nœud de logique qu'on
modifie ailleurs. Mais ça veut dire que delete_element.py, qui ne
supprimait jusqu'ici que la ligne elle-même, faisait échouer PRAGMA
foreign_keys=ON (db/connection.py) dès qu'un nœud de logique existant
référençait encore l'élément.

Fix : delete_element.py nettoie maintenant ces références AVANT de
supprimer l'élément - pas seulement pour l'élément explicitement supprimé,
mais pour tous ses DESCENDANTS aussi (leur suppression est cascadée
automatiquement au niveau SQL via parent_id, sans repasser par ce
fichier, donc sans ce nettoyage si on ne le fait pas explicitement).

Ajoute tests/test_delete_element_referenced_by_flow.py (élément
référencé comme déclencheur, comme cible d'action, et cas d'un conteneur
supprimé dont un descendant est référencé).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:19:52 +02:00
williamandClaude Sonnet 5 4dc42a7f3b Corrige les réglages fantômes d'un exemplaire d'élément de jeu déjà posé
Bug remonté : la taille d'un Titre réglée à 18px dans le modèle affichait
21px "sur l'écran". Cause : un exemplaire posé AVANT le passage en mode
"toujours lié au modèle" (tour précédent) avait été créé par l'ancien
mécanisme instantiate_template_tree (retiré), qui copiait tout l'arbre en
base - ces lignes copiées (l'ancien enfant "Titre", encore à 21px depuis
avant le changement) ne sont plus jamais RENDUES (le contenu vient
désormais toujours en direct du modèle, voir le tour précédent), mais
restaient toujours sélectionnables dans l'arborescence de l'éditeur, avec
leurs propres réglages jamais synchronisés. Cliquer dessus dans l'arbre
affichait donc ses vieux réglages (21px) dans le panneau de propriétés,
donnant l'impression trompeuse que le modèle (18px) n'était pas pris en
compte - alors que le rendu réel utilisait déjà correctement 18px.

Fix, dans list_elements.py : tout élément dont un ANCÊTRE a
element_type_id réglé est maintenant exclu de la liste renvoyée à
l'éditeur (arborescence, sélection, panneau de propriétés) - son contenu
n'a plus aucune existence propre, seul l'écran-modèle fait foi. Les
lignes elles-mêmes restent en base (pas de suppression, un simple filtre
en lecture) mais ne sont plus jamais atteignables depuis l'éditeur.

Ajoute un test de régression qui simule exactement ce scénario (ligne
orpheline avec un ancien réglage figé) et vérifie qu'elle n'apparaît plus
nulle part - ni dans le canevas, ni via une sélection directe par id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:10:30 +02:00
williamandClaude Sonnet 5 94f1414506 Corrige un bug important : le style de TOUT élément neuf était cassé depuis l'ajout de l'Ombre
Bug remonté ("toutes les propriétés ne sont pas prises en compte") : la
vraie cause n'avait rien à voir avec les éléments de jeu — le contrôle
"Ombre" (voir le tour précédent) a une valeur par défaut composite (un
dict Python, {"x":0,"y":4,...}, pas une chaîne CSS). default_style_for_
widget.py, qui fige les réglages "dont la valeur par défaut a un effet
visuel voulu dès la création" pour chaque widget neuf, n'excluait pas ce
nouveau type de contrôle — il écrivait donc ce dict TEL QUEL (repr Python)
dans le style de CHAQUE élément fraîchement créé depuis ce commit, quel
que soit son widget. Une valeur CSS invalide au milieu du style pouvait
donner l'impression que "plein de propriétés" ne s'appliquaient plus.

Deux correctifs :
1. default_style_for_widget.py exclut maintenant "shadow" du gel à la
   création (même raisonnement déjà appliqué à "color" juste au-dessus :
   la valeur par défaut n'est qu'une suggestion affichée dans le panneau,
   pas un réglage neutre à figer - le neutre CSS est "pas d'ombre").
2. style_string.py ignore désormais toute valeur non scalaire (dict/liste)
   au moment de construire l'attribut style - filet de sécurité pour les
   éléments déjà créés AVANT ce correctif, qui portent encore ce dict figé
   en base et continueraient sinon à s'afficher cassés.

Ajoute deux tests de régression dans test_shadow_controls.py (aucune ombre
au premier rendu d'un élément neuf ; un élément déjà corrompu avant ce
correctif continue de s'afficher normalement).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:54:06 +02:00
williamandClaude Sonnet 5 e9a991ed13 Un exemplaire d'élément de jeu posé sur un écran reste maintenant lié à son modèle
Jusqu'ici, poser un élément de jeu depuis le catalogue ("Mes éléments de
jeu") copiait tout son arbre en base (instantiate_template_tree) : chaque
exemplaire devenait indépendant, y compris de son propre modèle - modifier
l'élément de jeu dans son éditeur n'avait plus aucun effet sur les
exemplaires déjà posés ailleurs.

Change ce comportement pour qu'un exemplaire reste TOUJOURS lié à son
modèle, sur le même principe déjà utilisé par un modèle de ligne de
Répéteur (jamais copié, rechargé en direct à chaque affichage - voir
_load_template_tree/_render_repeater) : add_element.py ne crée plus
qu'UNE SEULE ligne plate (avec sa position/taille propres à cet
exemplaire) au lieu de copier tout l'arbre, et render_element_html.py
recharge le contenu depuis l'écran-modèle à chaque rendu quand
element_type_id est réglé. Modifier l'élément de jeu dans son propre
éditeur met donc à jour tous ses exemplaires déjà posés, sur n'importe
quel écran (y compris ceux placés AVANT ce correctif, qui portaient déjà
element_type_id sur leur ligne de premier niveau), sans avoir à les
retoucher un par un.

Contrepartie assumée (discutée avec l'utilisateur avant ce changement) :
un exemplaire ne peut plus être personnalisé individuellement à
l'INTÉRIEUR (texte, couleur d'un enfant précis...) - seules sa position et
sa taille sur l'écran restent propres à chaque exemplaire. Pour changer le
contenu, il faut désormais passer par l'éditeur de l'élément de jeu
lui-même.

instantiate_template_tree.py, devenu inutilisé, est supprimé.

Ajoute tests/test_element_type_live_instances.py (mise à jour d'un
exemplaire déjà posé, propagation jusqu'à "Jouer", position toujours
indépendante par exemplaire) et met à jour un commentaire de test devenu
obsolète dans test_screens_and_elements.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:26:45 +02:00
williamandClaude Sonnet 5 79fe04c6ac Corrige "database is locked" causé par une connexion SQLite qui fuit après un plantage
Bug remonté : suppression d'un élément échouant avec
sqlite3.OperationalError: database is locked, exactement sur le
conn.execute() de delete_element.py. La trace complète montrait que la
connexion attendait puis lâchait après le timeout (10s) - pas une simple
collision passagère entre deux requêtes (déjà gérée par WAL + busy_timeout,
voir les commentaires existants de connect()), mais un verrou tenu bien
plus longtemps : une connexion ouverte par une requête ANTÉRIEURE qui a
planté, jamais fermée.

Cause de fond : chaque fonction de db/ (~80 d'entre elles) ouvre sa propre
connexion et est censée la fermer elle-même avant de rendre la main - si
une exception survient entre l'ouverture et cette fermeture, le
conn.close() prévu n'est jamais atteint. En mode debug (voir app.py), le
débogueur Werkzeug garde la trace complète de l'erreur en mémoire pour
l'inspection interactive, ce qui inclut la variable locale `conn` :
empêchée d'être ramassée par le GC, elle ne libère jamais son verrou
d'écriture SQLite - bloquant TOUTE écriture suivante jusqu'au redémarrage
du serveur, même longtemps après l'erreur d'origine et sans lien apparent
avec elle (d'où la confusion : l'erreur semble venir de l'action qui
échoue, alors qu'elle est victime d'une fuite antérieure).

Fix, dans db/connection.py, sans toucher aux ~80 fonctions existantes :
connect() enregistre maintenant chaque connexion sur le contexte de la
requête Flask en cours (flask.g, uniquement quand il y en a un - un appel
direct hors requête, scripts/tests, n'est pas concerné) ; un
teardown_request ferme toute connexion encore ouverte à la fin de CHAQUE
requête, qu'elle ait réussi ou planté (garanti par Flask, contrairement à
after_request). Fermer une connexion déjà fermée normalement ne fait
rien, donc aucun changement de comportement pour le cas normal.

Ajoute tests/test_db_connection_leak_safety_net.py, qui reproduit le
scénario exact (connexion ouverte puis exception avant fermeture) et
vérifie qu'une écriture suivante ne bloque plus - désactivé temporairement
pour confirmer que le test échoue bien (et de la même façon) sans le fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:06:16 +02:00
williamandClaude Sonnet 5 bda082bffd Ajoute une propriété "Ombre portée" (box-shadow), disponible sur tout widget
Nouveau groupe "Ombre" dans le panneau de propriétés, à côté de "Bordure"
(même universalité — voir UNIVERSAL_CONTROLS) : décalage X/Y, flou,
étendue, couleur et opacité, combinés en une seule valeur CSS box-shadow
(couleur+opacité fusionnées en rgba(), un <input type="color"> seul ne
portant pas de canal alpha). Décalages/flou/étendue tous à 0 = pas
d'ombre, même convention que border-width à 0 = pas de bordure.

Nouveau ctype "shadow" (c_shadow.py), suit exactement le même principe
que le ctype "size" déjà existant (size_override_controls.py) : plusieurs
entrées de formulaire pour un seul réglage, composées/décomposées dans
save_element_controls.py et control_value.py plutôt que passées par le
chemin générique clé->valeur.

Ajoute tests/test_shadow_controls.py (présence dans le panneau,
composition de la valeur CSS, aller-retour dans le formulaire, remise à
zéro qui retire l'ombre).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 14:37:38 +02:00
williamandClaude Sonnet 5 9c9e3c5379 Le sélecteur de champ apparaît maintenant sur tout nouveau widget d'un élément de jeu lié à un objet
Le tour précédent exigeait que CHAQUE widget règle sa propre "Donnée
liée" (_data_definition_id) pour voir apparaître le sélecteur de champ -
mais un élément de jeu ("Mail card"...) créé avec un "Objet lié" (voir
element_types.html, bound_definition_id) a précisément pour but d'éviter
ce réglage widget par widget : ses {{champ}} sont censés venir de CET
objet, fourni plus tard par le Répéteur qui l'utilisera comme modèle de
ligne. D'où le bug remonté : un nouveau Titre/Texte posé dans un tel
élément de jeu n'affichait jamais le sélecteur.

Ajoute get_element_type_by_template_screen(slug, screen_id), pour
retrouver depuis l'éditeur d'un écran-modèle l'entrée du catalogue (et
donc l'objet lié) dont il est la recette. screen_edit.py le calcule pour
le panneau de propriétés et le passe à controls_with_values(), qui
l'utilise comme repli pour le champ "Contenu" SEULEMENT si ce widget n'a
pas déjà sa propre "Donnée liée" réglée (priorité conservée au réglage le
plus spécifique).

Ajoute tests/test_element_type_bound_field_picker.py (apparition sans
réglage supplémentaire, absence sans objet lié, priorité à la "Donnée
liée" du widget si réglée).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 13:38:10 +02:00
williamandClaude Sonnet 5 7e80ff952a Ajoute un sélecteur de champ pour insérer {{champ}} dans le Contenu
Une fois "Lier à un objet de données" réglé (widget Texte/Titre...),
propose maintenant les champs de CET objet en liste déroulante juste sous
le champ "Contenu", avec un bouton "+ Ajouter" qui insère "{{nom_du_champ}}"
à l'emplacement du curseur - plutôt que d'avoir à taper cette syntaxe à
la main sans savoir quels noms de champs existent réellement.

controls_with_values.py pose field_options sur le contrôle "content"
quand _data_definition_id est réglé (réutilise data_binding_options, déjà
calculé pour data_filtre_champ/data_filtre2_champ) ; insertFieldAtCursor()
(screen_edit.html) fait l'insertion via selectionStart/selectionEnd puis
déclenche un événement "input" pour que l'enregistrement automatique se
déclenche normalement, comme une saisie manuelle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 13:18:12 +02:00
williamandClaude Sonnet 5 baa3da1035 Corrige la comparaison booléenne : "Oui"/"Non" n'était pas reconnu comme valeur vraie/fausse
Bug remonté avec une "Mail card" (icône enveloppe fermée si is_opened
est à "Non", ouverte si "Oui") : les DEUX variantes s'affichaient (ou
aucune), selon la ligne.

Cause : _compare() (filter_repeater_rows.py, utilisé par la condition de
visibilité, le Répéteur et Donnée liée) ne reconnaissait "1"/"true"/"vrai"
comme valeur vraie pour un champ booléen — jamais "oui", pourtant le SEUL
vocabulaire que l'app affiche elle-même pour ce type de champ partout
ailleurs (voir data_list.html : "Oui" si vrai sinon "Non"). Une valeur de
comparaison fixe tapée "Oui" retombait donc silencieusement à "faux",
et comme l'opérateur et le champ étaient par ailleurs corrects, ça
donnait l'impression que la condition "ne voyait" rien : sur la ligne où
is_opened=faux, les DEUX cartes ("égal à Oui" et "égal à Non", toutes
deux évaluées comme "égal à faux") s'affichaient ensemble ; sur la ligne
où is_opened=vrai, aucune des deux.

Fix : "oui" ajouté à l'ensemble des valeurs reconnues comme vraies,
côté Python (_compare) ET côté JS (compareValues() dans play.html, qui
doit rester alignée — utilisée par les nœuds Condition de la Logique de
la scène), cette dernière au passage rendue insensible à la casse comme
son équivalent Python (elle ne l'était pas du tout).

Ajoute un test de régression dédié à ce cas précis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:52:27 +02:00
williamandClaude Sonnet 5 a28cc5881e Corrige la condition de visibilité : un widget "special_render" visible ne se rafraîchissait jamais en jeu
Bug remonté avec deux icônes (enveloppe fermée / ouverte) posées
directement sur l'écran, chacune conditionnée sur is_opened : après avoir
ouvert le mail (une action "Modifier une donnée"), les DEUX icônes
restaient affichées en même temps au lieu que l'ouverte remplace la fermée.

Cause : refreshRuntimeData() (play.html) ne réévalue, après une action,
que les éléments dont le HTML porte un marqueur ("visibilityGated"/
"repeaterItem"/"jaugeBar"). render_element_html() posait bien ce marqueur
quand un élément sous condition est actuellement visible - mais seulement
sur le chemin de rendu GÉNÉRIQUE (texte, titre, conteneur...), jamais sur
les 9 widgets "special_render" (Icône, Tableau, Superposition, Onglets,
Case à cocher, Liste déroulante, Groupe de champs, Répéteur, Jauge) - un
élément CACHÉ portait toujours son marqueur (via son placeholder), mais un
élément VISIBLE de ce type non. Résultat : l'icône "fermée", visible au
premier chargement, ne portait aucun marqueur et restait donc figée dans
son état d'origine après toute action suivante, pendant que l'icône
"ouverte" (cachée au départ, donc marquée) se mettait, elle, correctement
à jour - d'où les deux affichées ensemble.

Fix : les 9 branches special_render passent maintenant, elles aussi, par
_mark() comme le chemin générique.

Ajoute un test de régression dédié (icône visible sous condition = doit
porter le marqueur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:31:50 +02:00
williamandClaude Sonnet 5 ff01b3903c Corrige la condition de visibilité (mode "objet") à l'intérieur d'un Répéteur
Bug remonté : dans une "Mail card" (élément de jeu réutilisable, deux
icônes - enveloppe fermée/ouverte - conditionnées sur le champ is_opened
de l'objet Email) posée dans un Répéteur de données, rien ne s'affichait
jamais correctement.

Cause : is_element_visible() (mode "objet") allait toujours chercher en
base la ligne la plus récente de l'objet ciblé (convention "1 seule ligne
= état de partie", correcte pour une Jauge suivant un état de partie),
sans jamais tenir compte de la ligne EN COURS DE RENDU dans un Répéteur -
donc tous les exemplaires du même modèle de ligne évaluaient la MÊME
ligne (la plus récente de tout l'objet Email) au lieu de chacun la
sienne, et affichaient donc tous exactement le même résultat.

Fix : is_element_visible() reçoit maintenant le ctx de rendu (les
{{champ}} de la ligne en cours, déjà posés par render_repeater.py) et,
si le champ réglé s'y trouve, utilise directement cette valeur plutôt que
d'interroger la base - un exemplaire de Répéteur voit donc bien SA propre
ligne. Hors Répéteur, le comportement (ligne la plus récente de l'objet)
est inchangé.

Ajoute tests/test_visibility_condition.py (mode variable, mode objet hors
Répéteur, absence dans l'éditeur, et ce cas précis dans un Répéteur) -
cette fonctionnalité n'avait jusqu'ici aucun test persistant, seulement
des scripts ad-hoc jetés après vérification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:17:40 +02:00
williamandClaude Sonnet 5 11b08c8503 Éditeur de scène : le canevas remplit exactement l'espace disponible, plus de marges
Le calcul JS "object-fit:contain" du tour précédent gardait l'aspect-ratio
(Portrait/Paysage/Carré) au prix de marges vides sur les côtés dès que la
fenêtre n'avait pas exactement ce ratio - "prendre toute la place
disponible" et "garder l'aspect-ratio" sont deux exigences contradictoires
dans ce cas, et c'est la première qui doit l'emporter dans l'éditeur.

Le canevas (#canvas) remplit donc maintenant .canvasFrame à 100% x 100%,
sans plus tenir compte de l'aspect-ratio choisi dans l'éditeur - ce
réglage continue de s'appliquer normalement à l'aperçu jouable ("Jouer",
voir screen_set_aspect.py/play.html), qui reste la référence pour le
rendu final. Padding de .canvasFrame réduit au minimum. fitCanvasToFrame()
et son calcul en pixels n'ont plus lieu d'être - retirés entièrement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 11:50:50 +02:00
williamandClaude Sonnet 5 14deba943a Éditeur de scène : le canevas remplit vraiment tout l'espace disponible
Le calcul purement CSS du tour précédent (aspect-ratio + height:100% +
max-width:100%, pour que le canevas garde ses proportions tout en tenant
dans la zone visible) laissait en pratique le canevas bien plus petit que
l'espace réellement disponible - le calcul de taille "auto" d'un élément
non remplacé dans ce contexte flex n'est pas fiable.

Remplacé par un calcul en JavaScript (fitCanvasToFrame()) : mesure la
taille réelle de .canvasFrame et calcule la plus grande taille en pixels
qui tient à la fois en largeur ET en hauteur pour l'aspect-ratio courant
(l'équivalent d'un "object-fit:contain"), posée directement en style
inline sur #canvas. Recalculé à l'ouverture de l'écran, au changement de
format (Portrait/Paysage/Carré), au redimensionnement de la fenêtre, et à
chaque rafraîchissement du panneau (#canvas étant recréé à chaque
sélection d'élément, sa taille calculée était perdue à chaque fois).
Sous 1300px (mise en page empilée), le style inline est explicitement
effacé pour laisser la règle CSS de repli (pleine largeur, page qui
défile normalement) reprendre la main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 11:44:40 +02:00
williamandClaude Sonnet 5 dae6da166e Éditeur de scène : supprime le fil d'Ariane, canevas remonté, défilement propre au canevas
Le fil d'Ariane disparaît entièrement sur cette page (breadcrumb_wrap vide)
- le nom de l'écran est de toute façon déjà éditable juste au-dessus, dans
le panneau "Éléments" (voir le tour précédent) - et .content-builder perd
son padding par défaut hérité de .content, pour que le canevas commence le
plus haut possible.

Change aussi la façon dont le canevas est dimensionné : il était
jusqu'ici contraint par la LARGEUR (width:100%), ce qui pouvait le rendre
bien plus haut que la fenêtre pour un format Portrait - obligeant à
défiler .builderCanvasArea (toolbar/onglets compris) pour voir le bas de
l'écran. Il est maintenant contraint par la HAUTEUR disponible
(height:100%, la largeur se déduisant de l'aspect-ratio), avec
max-width:100% en secours si c'est la largeur qui manque en premier -
l'écran entier reste donc visible sans défiler. Le défilement, s'il reste
nécessaire (fenêtre très basse), se fait maintenant sur .canvasFrame
lui-même, jamais sur .builderCanvasArea (repassé à overflow:hidden) : la
barre d'onglets et la barre d'outils restent toujours fixes en haut.

Ajoute le pendant pour le repli en page empilée (< 1300px, une seule
colonne) : le "letterboxing" par hauteur suppose une chaîne de hauteurs
définies qui n'existe plus une fois empilé - revient alors à un
dimensionnement par largeur, cohérent avec une page qui défile normalement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 11:38:29 +02:00
williamandClaude Sonnet 5 ea9f91fc19 Éditeur de scène : Logique/Timeline en onglets plein écran, réglages d'écran déplacés dans le panneau Éléments
Remplace l'ancien panneau du bas rétractable/redimensionnable à la souris
(Logique de la scène, Timeline d'animation) par un système d'onglets au
centre de l'éditeur - Écran / Logique de la scène / Timeline d'animation
- un seul visible à la fois, occupant systématiquement tout l'espace
disponible (switchBuilderTab() dans screen_edit.html). Changer d'onglet
équivaut à "fermer" celui qu'on quitte ; plus besoin d'une poignée de
redimensionnement séparée puisque l'onglet actif prend déjà toute la
place. Supprime au passage tout l'ancien mécanisme (toggleFlowPanel/
toggleAnimPanel, poignées flowResizeHandle/animResizeHandle, classe CSS
.flowPanel) devenu inutile.

Déplace aussi le renommage de l'écran, le bouton "Jouer depuis le début"
et le choix du format d'aperçu (Portrait/Paysage/Carré) - jusqu'ici
au-dessus du canevas - dans le panneau flottant "Éléments" (celui qui
porte déjà ce nom, à gauche) : des réglages qu'on touche rarement une
fois l'écran en construction, qui n'ont plus besoin de rester en
permanence visibles au-dessus de la zone de travail.

Corrige au passage un bug découvert pendant ce tour : sur la page "Nouvel
objet" (2 colonnes), le bouton "+ Ajouter un champ" avait disparu -
placé APRÈS la zone de liste à défilement (flex-grow:1) dans la colonne
de droite, un flex-grow imprévisible dans ce contexte le poussait hors de
vue. Déplacé avant la liste (statique, toujours visible), pattern déjà
éprouvé ailleurs sur cette même page.

Deux tests mis à jour pour refléter intentionnellement la nouvelle
structure : la présence de "animTabPanel" (au lieu de l'ancien
"animPanel"), et un marqueur plus précis pour distinguer le bloc de
propriétés "Onglets" d'une simple occurrence du même texte dans une liste
déroulante de la Logique de la scène (qui apparaît désormais plus tôt
dans le document, cet éditeur de flow étant maintenant un onglet du
centre plutôt qu'un panneau tout en bas de page).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 11:27:50 +02:00
williamandClaude Sonnet 5 2051b375ac Passe toutes les pages restantes en deux colonnes (créer à gauche, contenu à droite)
Généralise le principe déjà utilisé pour le tableau de bord et l'édition
d'objet à toutes les pages restantes : liste des jeux, écrans, éléments de
jeu, variables, nouvel objet, formulaire de données, tableau des données
d'un objet - colonne de gauche pour créer/agir, colonne de droite pour ce
qui existe déjà, chaque colonne défilant pour son propre compte.

Pour un formulaire qui doit rester UN SEUL <form> à cheval sur les deux
colonnes (nouvel objet : nom à gauche, champs à droite ; formulaire de
données : bouton Enregistrer à gauche, champs à droite), nouvelle classe
.formPassthrough ("display:contents") : le <form> ne devient pas lui-même
une boîte dans la mise en page flex, seul .twoCol à l'intérieur compte.

Supprime au passage .content-page/.formScroll/body.pageBody (le gabarit à
une colonne introduit au tour précédent, plus utilisé nulle part) et
.list/.listRow/.listRowFlex/.rowTitle/.rowSub/.rowActions/.addBtn (les
cartes à deux lignes remplacées par les tableaux denses et la nav
compacte) - du CSS mort plutôt que deux systèmes qui se chevauchent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:58:49 +02:00
williamandClaude Sonnet 5 59247694d6 Applique le gabarit sobre/compact/sans défilement à toutes les pages hors éditeur
Généralise le principe déjà en place pour l'édition d'objet (la page
n'occupe jamais plus que la hauteur de la fenêtre, seules ses zones
internes défilent) à toutes les pages restantes : liste des jeux, tableau
de bord d'un jeu, écrans, éléments de jeu, variables, nouvel objet,
formulaire de données, tableau des données d'un objet. Seuls l'éditeur
d'écran/d'élément et l'aperçu jouable restent en dehors (déjà exclus par
leur propre gabarit plein écran, ou pas concernés).

Nouveau gabarit générique à une colonne (body.pageBody + .content-page +
.scrollArea) sur le même principe que .content-objectEdit, plus une
variante .formScroll pour un formulaire long à défilement interne (liste
de champs d'un nouvel objet, formulaire de données) tout en gardant les
boutons d'action toujours visibles. Les listes/tableaux remplacés par le
format dense .fieldsTable (déjà utilisé pour le tableau de bord) pour
rester cohérent et afficher plus de lignes à l'écran.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:45:42 +02:00
williamandClaude Sonnet 5 ac0f003663 Rend le tableau de bord du jeu plus compact, sobre et sans défilement de page
Réutilise le système déjà en place pour l'édition d'objet (body.objectEditBody
+ .content-objectEdit) : la page occupe exactement la hauteur de la fenêtre,
seules les deux colonnes défilent chacune de leur côté si besoin - plus la
page elle-même, qui ne doit jamais défiler.

Colonne de gauche ("Créer") : nouvelle nav à une ligne (.navRow/.navList,
icône + libellé, sous-titre en info-bulle) plutôt que des cartes à deux
lignes - plus dense, plus sobre.

Colonne de droite ("Objets définis") : remplace les cartes par le tableau
compact à en-tête collant déjà utilisé pour les champs d'un objet
(.fieldsTable) - format nettement plus adapté à beaucoup de lignes.

Réduit aussi le padding par défaut de .listRow/.dangerZone (encore utilisés
par la page Variables), dans le même esprit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:31:43 +02:00
williamandClaude Sonnet 5 20ef86e039 Aligne le style du bouton "nouvel objet" sur les autres actions du tableau de bord
Remplace le bouton en pointillés ("+ Définir un nouvel objet") par une
entrée de liste identique aux autres actions (Écrans, Variables, Jouer...)
— plus cohérent visuellement, comme demandé après revue du rendu réel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:24:07 +02:00
williamandClaude Sonnet 5 11a2d397e8 Remplace la création inline de variable par une page de gestion dédiée, redesign du tableau de bord du jeu
Retrait de la création rapide de variable globale depuis le sélecteur de
la Condition de visibilité (bouton "+ Créer") : une variable globale est
désormais gérée comme un objet "jeu" à part entière, avec une vraie page
CRUD ("Variables", nouvelle entrée du menu de gauche) - création, édition
du type/valeur, suppression. Le nom reste volontairement immuable après
création (c'est par ce nom qu'une condition de visibilité ou une action
"Modifier une variable" la référence - la renommer casserait ces réglages
en silence), d'où db.update_global_variable qui ne touche que type/valeur.

Redesign du tableau de bord du jeu (game_dashboard.html) en deux
colonnes : à gauche tout ce qu'on peut créer (écrans, éléments de jeu,
variables, jouer, nouvel objet) plus les paramètres du jeu (renommer/
supprimer) ; à droite ce qui a déjà été créé (objets définis). Remplace
les cartes Bulma par le système de mise en page compact déjà défini dans
style.css (.twoCol/.listRow/.dangerZone/.addBtn) mais jamais utilisé
jusqu'ici - plus dense et cohérent avec le reste de l'éditeur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:00:35 +02:00
williamandClaude Sonnet 5 c04bc0b926 Ajoute la condition de visibilité et les variables globales
Nouveau panneau "Condition de visibilité" disponible dans les propriétés
de TOUT élément (widget) : permet de masquer un élément en mode jouable
selon deux moyens, au choix -
  - une variable globale (nom + type + valeur, une seule par jeu, stockée
    dans une nouvelle table _global_variables) ;
  - le champ d'un objet de données existant (même convention "état de
    partie" - une seule ligne - déjà utilisée par la Jauge).

Une variable ne servant à rien si elle ne peut jamais changer en cours de
partie, ajoute aussi une nouvelle action de flow "Modifier une variable
globale" (parallèle à "Modifier une donnée"), avec sa propre route
d'exécution serveur et son sous-formulaire dans l'éditeur de logique de
scène. Une variable peut aussi se créer à la volée depuis le sélecteur du
panneau de visibilité, sans quitter les propriétés de l'élément.

La condition n'est évaluée qu'en mode jouable (/game/<slug>/play), jamais
dans l'éditeur, pour que l'élément reste toujours sélectionnable. Un
élément masqué se réévalue en direct après toute action "Modifier une
donnée/variable", via le même mécanisme de rafraîchissement déjà utilisé
par la Jauge et le Répéteur.

Corrige au passage deux bugs découverts en testant bout en bout : (1)
apply_ctx plantait sur le nouveau marqueur interne _forge_play_mode (un
booléen parmi les {{champ}} à substituer, qui attend des chaînes) ; (2)
_compare traitait toute valeur booléenne stockée en chaîne ("0" inclus,
donc toujours vraie en Python) comme vraie - correct pour les champs
d'objet (entiers SQLite) mais faux pour les variables globales (toujours
stockées en texte).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 09:34:10 +02:00
william 3c6d07cb3a Remove the "Ajouter DANS ce conteneur" properties-panel menu
Redundant since dragging an element onto a container row in the tree now does the same thing (see move_element_to_container.py). The element_add_child route itself stays (still used by tests and available as an API), only the UI entry point is gone. Removed the now-dead .containerContentGroup CSS along with it.
2026-08-25 07:22:09 +02:00
william aa3be503ce Expose relation fields in the champ pickers and fix their column resolution
data_definition_options() used to exclude relation-type fields from its field list entirely, so a filter/binding could never reference "the linked object" of a row — and even when a relation field's clean name was typed manually (as the earlier "level.parcour" example needed), it silently matched nothing: every column lookup for a filter/repeater field used the field's own name, but a relation is actually stored in a "<field>_id" column (see create_definition.py), so the lookup always missed.

Relation fields now appear in the champ dropdowns (Répéteur's filtre_champ/filtre2_champ, Donnée liée's data_filtre_champ/data_filtre2_champ) labeled with the object they point to (e.g. "parcour (→ parcours)"), and a new _field_column() helper in filter_repeater_rows.py resolves the right "<field>_id" column whenever the field turns out to be a relation — used consistently by the filter comparison itself, the "{{Objet.champ}}" dynamic-value resolver, the repeater's row content ({{champ}}), and the Donnée liée row context. Jauge's own champ/champ_nom pickers (which need an actual displayable value, not an id) still exclude relations, both server- and client-side.

Verified end to end: the dropdown shows the relation field with its target-object label, and filtering "level" rows by the clean relation field name "parcour" (not "parcour_id") against a dynamic {{game.current_parcours}} reference now actually matches, alongside the existing "number" filter. Full suite green (89).
2026-08-25 06:48:04 +02:00
william 343732d7aa Turn free-typed field/object names into dropdowns in the Répéteur and Donnée liée filters
Two settings previously required typing an exact field or object name by hand:

- "champ" (Répéteur's filtre_champ/filtre2_champ, Donnée liée's data_filtre_champ/data_filtre2_champ) is now a dropdown of the actual fields on the object already chosen for that widget — pre-populated server-side (controls_with_values.py) and live-updated client-side without a reload when you change the object (bindDefinitionFieldSelects(), generalizing the existing Jauge champ/row_id pattern).

- "valeur" (the comparison value, which could already reference another object's field via a "{{Objet.champ}}" string typed by hand) is now a "Valeur fixe" / "Donnée d'un autre objet" toggle — the dynamic mode shows an object dropdown and a dependent field dropdown, and picking from them reconstructs that internal reference string automatically (initFilterValuePickers()/updateFilterValueFromDynamic()). The underlying stored value and _resolve_filter_value's parsing are unchanged, so existing elements using the old typed syntax still load correctly and populate the pickers on open.

Guards against writing a literal "{{...}}" pair directly in the Jinja template source (caught by a real render error while building this — Jinja parses {{/}} anywhere in the file, including inside comments) by building that syntax from split string literals in JS instead.

Verified end to end: repeater panel renders the pickers, saving with a dynamic reference filters correctly against live data (current_level changing which row shows), and reloading pre-selects the right object/field in the dropdowns. Full suite green (89).
2026-08-25 06:32:21 +02:00
william 33a887e7ba Move icon gallery to the left panel, drop the redundant Icône tile, add drag-into-container in the tree
- The "icone" widget no longer appears as a generic tile in "Ajouter un élément sur l'écran" / "Ajouter DANS ce conteneur" — the icon gallery (now living under "Ajouter un élément sur l'écran" in the left panel instead of the right one) is the only way to add one, always pre-set to the icon you picked.
- The element tree now supports dragging an element onto a container row to move it inside (last child), from anywhere in the tree — not just reordering within the same parent. Hovering a container row splits it into before/after/inside zones (thirds) when dragging a sibling, or "inside only" when dragging from elsewhere in the tree. New move_element_to_container() rejects non-container targets and cycles (dropping a container into itself or one of its own descendants) silently, mirroring reorder_element's existing safety pattern.

Verified end to end: gallery renders in the left panel with no icone tile in the widget grids, and the move endpoint correctly reparents, rejects a cycle, and rejects a non-container target. Full suite green (89).
2026-08-25 06:03:02 +02:00
william 6d80ed6958 Switch icons from a webfont to self-hosted SVG masks — the webfont was the actual problem
Self-hosting Font Awesome's font files (previous commit) didn't fix it either: every icon in the picker still failed identically. That rules out CDN/network blocking specifically and points to something in the browser blocking custom webfont loading altogether regardless of origin (common with strict anti-fingerprinting protection, e.g. Brave's font blocking or certain privacy extensions — @font-face loading is a well-known fingerprinting vector).

Replaced the whole mechanism: each of the 258 curated icons is now a real downloaded SVG file (static/icons/*.svg, sourced from Font Awesome 5 Free's official SVG package) displayed via CSS mask-image (.icon-svg in style.css) instead of a font glyph. A masked SVG isn't a font resource at all, so it isn't subject to webfont-blocking — and it still colors via the existing "color" style property (background-color:currentColor) and sizes via font-size (1em), so no change to how the widget's other controls work.

The "Icône" widget now stores just the icon's slug (e.g. "trophy") in a dedicated attribute instead of a full CSS class string, rendered by a new special_render (render_icone.py). Updated the add-gallery and the properties-panel picker modal to use the same mask technique for their previews, and element_add.py to validate the slug against the curated list. Removed the now-unused self-hosted Font Awesome CSS/webfont files and the <link> tags — no font dependency left for icons at all.

Verified end to end (gallery renders with real SVG previews, simulated add creates a correctly-attributed element, the SVG file is actually served, the per-element picker reflects the current icon). Full suite green (89).
2026-08-24 20:11:49 +02:00
william 15cad6c50f Self-host Font Awesome instead of loading it from a CDN
Switching CDN provider (cdnjs -> jsdelivr) didn't fix icons rendering as fallback glyph-code text — every icon in the picker still failed the same way, which points to something in the user's browser/network blocking cross-origin webfont loading specifically (common with strict tracking/fingerprinting protection, e.g. Brave's font-blocking or an ad-blocker's remote-font rule), not the CDN itself.

Downloaded Font Awesome 5.15.4's CSS and the solid-weight webfont files (the only style this app's icon classes actually use) into static/fontawesome/, served same-origin like style.css and pjax.js already are. Removes the CDN dependency entirely for icons rather than betting on a second provider.
2026-08-24 20:01:39 +02:00
william 675316cb3d Switch Font Awesome CDN from cdnjs to jsdelivr
Icons weren't rendering (glyph placeholder text like "F085" showing instead of the actual icon — the browser's font-fallback behavior when no font can render that codepoint, meaning the FA webfont never loaded). Both CDN URLs resolve fine from here, but Bulma (already jsdelivr) renders correctly for the user while cdnjs-hosted Font Awesome didn't — switching to the same jsdelivr provider removes that difference as a variable.
2026-08-24 19:57:31 +02:00
william 9cc6704e08 Replace the icon widget's plain text field with a visual searchable picker
The properties panel's "Icône" setting was a raw text input expecting a Font Awesome class name typed by hand — not something a non-developer using Forge Engine should have to know. It's now a button showing the current icon that opens a modal with the same searchable icon grid as the add-gallery; picking one replaces the selected element's icon and autosaves immediately, no typing required. The class name is still stored the same way (a hidden input feeding the existing generic attr:class control), so no backend changes were needed.
2026-08-24 19:26:35 +02:00
william cc0442ef5f Add a Font Awesome icon gallery to the properties panel, draggable onto the screen
New "Icône" widget (<i class="...">, color/size customizable exactly like any other widget) plus a curated set of ~250 verified Font Awesome 5 Free solid icon names (fontawesome_icons.py) — not the full ~1500-icon catalog, since an embedded list needs to be guaranteed accurate (a wrong class name silently renders as a blank glyph); any other valid FA5 class still works by typing it directly into the widget's "Icône" setting.

The gallery lives in the (now always-visible) top of the right floating panel, searchable, with each tile both clickable and HTML5-draggable onto the canvas — either action posts to element_add with the chosen icon_class, which now seeds the new element's class instead of leaving it on the generic default. Available in the element-type template editor too, since it reuses screen_edit.html.

Loads Font Awesome 5.15.4 (cdnjs) alongside the existing animate.css/Bulma links, in both the editor and /play.

Verified end to end: gallery renders with real icon glyphs, clicking/simulated-drop creates a correctly-classed <i> element, and the actual /play route renders it with Font Awesome loaded. Full suite green (89).
2026-08-24 19:10:25 +02:00
william 850242d1d4 Make the left element-tree panel floating/draggable/closable too, same as the right one
Generalized the floating-panel mechanism from the properties panel (props/propsPanelFloat/propsPanelReopenBtn) into reusable functions keyed by a short id, and applied it to the left "Éléments de cet écran" panel (key "left", panelLeftFloat, panelLeftReopenBtn). Both panels can now be closed independently, letting the canvas take the screen's full width for the most faithful possible preview against the actual /play rendering. Shared CSS (.floatPanel/.floatPanelHeader/.floatPanelBody/.floatPanelReopenBtn with --left/--right position variants) replaces the props-panel-only rules from the previous commit.

Fixed the two querySelector('.builderPanel .elementTree') call sites (used to patch the tree preview into the DOM after an unrelated canvas-only refresh) that would have silently stopped finding the tree once its wrapper's class changed.
2026-08-24 15:33:47 +02:00
william 03ca2723c5 Make the screen editor's properties panel floating, draggable, and closable
The right-hand properties panel was a fixed 320px flex column, permanently shrinking the canvas — on a screen with real content (tabs, gauges), this made the editor's preview visibly narrower than the actual /play rendering, to the point of wrapping tab labels differently. It's now position:fixed with a drag handle header, moved out of .builder3's flex flow so .builderCanvasArea automatically reclaims the space; closing it (✕, replaced by a small "⚙️ Propriétés" reopen button) frees the full width for a more faithful preview. Position and open/closed state persist per screen in localStorage and get reapplied after every selection change (which replaces #builder3 wholesale). Falls back to a normal stacked column below 1300px width, where a floating panel wouldn't fit usefully.
2026-08-24 15:12:50 +02:00
william a7518355ed Expose every Bulma variant as a choosable option, not just a curated few
Adds a shared bulma_variants.py (color/size option lists) and expands BULMA_*_CONTROLS in bulma_controls.py to cover each component's full modifier set: Bouton (colour incl. white/light/dark/black/text, light-shade toggle, size, rounded, outlined, inverted, static, loading, fullwidth), Titre (Bulma size is-1..is-6 independent of heading level, is-spaced), Jauge (size), Tableau (bordered/fullwidth/striped/hoverable/narrow, now toggleable instead of hardcoded), Onglets (alignment, boxed/toggle/toggle-rounded style, size, fullwidth), and every form field — champ_texte/email/mot_de_passe, zone_texte, liste_deroulante (colour, size, rounded, static).

render_select.py/render_onglets.py/render_jauge.py now redirect the merged "class" from _visible_attrs to the actual Bulma sub-element (the .select wrapper, the .tabs div, the <progress> tag) instead of the outer positioning wrapper, since that's what needs to carry the modifier classes.

Also fixed a real latent bug found while wiring this up: a checkbox control with default=True (e.g. Tableau's "Première ligne = en-tête") was never actually applied on a freshly created element — default_style_and_attributes unconditionally skipped ALL checkbox types at creation, so the panel showed it checked while the element itself had nothing set. Now a checkbox's default=True is frozen at creation like any other meaningful default; default=False (the common case) is unaffected.

Verified end to end: every widget's variant controls save and render the right class tokens (spot-checked titre/tableau/onglets/champ_texte/liste_deroulante/jauge), full suite green (89 passed).
2026-08-24 14:50:44 +02:00
william bb01e9fdf1 Make Jauge, Onglets, and every form-field widget real native Bulma elements
- Jauge now renders a real <progress class="progress"> instead of a hand-built pair of absolutely-positioned divs. The bas/haut color interpolation still works, set via Bulma's own --bulma-progress-value-background-color CSS custom property rather than fighting the class.
- Onglets' tab strip is now genuine Bulma tabs markup (tabs > ul > li, is-active on the li) instead of custom forgeTabBar/forgeTabBtn classes; forgeShowTab (duplicated in screen_edit.html and play.html) now toggles is-active to match.
- Champ texte/email/mot de passe get class="input", Zone de texte gets class="textarea", Case à cocher/Bouton radio's existing <label> wrapper gets class="checkbox"/"radio" (Bulma's own convention — the structure already matched, just needed the class), Liste déroulante is wrapped in Bulma's required <div class="select"> (a bare class on the <select> itself has zero effect in Bulma), Tableau gets class="table is-bordered is-fullwidth" with the per-cell inline borders removed so Bulma's own table styling applies.
- Removed the now-dead .forgeTabBar/.forgeTabBtn CSS.

Updated tests/test_jauge.py and tests/test_onglets_widget.py assertions to match the new markup (value="X" attribute instead of width:X% inline style, --bulma-progress-value-background-color instead of background-color, forgeShowTab(this) marker instead of the removed forgeTabBtn class) — same behavior, different rendering mechanism. Full suite green (89 passed), verified end to end against the real "test" project via the actual /play route.
2026-08-24 14:31:55 +02:00
william 24ced267cb Stop zeroing out padding/margin/border-width on native-Bulma widgets
Every widget gets padding/margin/border-width reset to 0 inline at creation (needed for a free-positioning canvas, otherwise every <h1>/<ul>/<p> would carry the browser's default spacing) — but for a widget that carries an external class like Bulma's button/box (see fixed_attrs in registry.py), inline style always wins over the class, so this reset silently stripped Bulma's own padding and border, making them look nothing like the real components. Bouton/Conteneur/Titre now skip this reset for those three properties, letting Bulma's own box model show through until a value is actually customized.

Also cleaned the frozen 0px values out of existing elements in the two live project databases (projects/test, projects/test-2) — only removed values that exactly matched the old frozen default, any genuinely customized non-zero padding/margin/border-width was left untouched.
2026-08-24 14:01:38 +02:00
william 2a673e023c Make Conteneur a native Bulma box by default instead of an opt-in style picker
Dropped the "Style Bulma" select that required manually choosing "Carte" to get Bulma's box class — the widget now always carries class="box", same unconditional pattern as Bouton (button) and Titre (title). A custom background color still overrides it the moment it's set, same as before.
2026-08-24 13:45:29 +02:00
william 8da5715e5b Make the screen editor's properties panel use native Bulma elements, not just Bulma-styled buttons
The element header/rename/id row, tabs list, position & size grid, and — most importantly — the generic control-rendering loop (used by every widget's properties panel: text, color, slider, checkbox, align, select...) now emit real field/control/label/select/checkbox/buttons-has-addons Bulma markup instead of the old custom controlRow/sliderRow/alignGroup/posGrid CSS. Removed the now-redundant custom CSS those classes used to carry.

Verified structurally safe: bindPropsAutosave/submitPropsForm already used querySelectorAll/FormData (structure-agnostic), so wrapping inputs in field/control divs doesn't affect autosave; the one sibling-dependent bit (slider oninput reading nextElementSibling) was kept adjacent inside its wrapper. Confirmed end-to-end with a live save round-trip through the actual route (Bulma modifier + free-style values both persist correctly) and every widget type's panel still renders 200 OK.

The flow-graph node editor (bottom panel) is not converted yet — part of its markup is built dynamically in JS (renderFlowConditionClauses), so reskinning it means updating the JS templates in lockstep with the HTML, which is more work/risk than this pass; noting it as the next piece.
2026-08-24 13:30:17 +02:00
william 25a331830b Reskin the whole admin UI (not just game widgets) with Bulma
Loads Bulma 1.0.2 in base.html (data-theme="dark" for its native dark palette) for every page that extends it, plus play.html directly. Converts every admin template — index, game dashboard, screens list, element types, object new/edit, data form/list — to real Bulma markup: navbar, breadcrumb component, box/card, field/control/input/select, table, notification, buttons, and a native Bulma modal for the data-row detail popup. Forge's own style.css keeps only what Bulma doesn't cover (the fixed-viewport builder/object-edit layouts, the element tree, canvas, flow-graph editor) and now acts as a secondary/override layer rather than a competing design system, matching how per-element inline customization already overrides widget defaults.

screen_edit.html (the 3-panel screen builder) gets the same navbar/breadcrumb/button treatment plus its top rename form, but its flow-graph node editor, canvas and element tree keep their existing custom styling — several of their inputs have JS relying on exact DOM sibling structure (e.g. slider oninput reading nextElementSibling) or Bulma's own select/wrapper requirement, and reskinning them without a browser to verify against risked silently breaking the app's most complex feature. Buttons and headings there are still fully converted (safe, purely additive class changes).

Verified: full test suite green, every route smoke-rendered 200 OK via the test client after the change.
2026-08-24 13:19:02 +02:00
william 38f33d88d9 Make Bulma the default style engine for Bouton/Titre/Conteneur, layered under existing inline customization
Adds a new "class:" control-target kind (save_element_controls.py, _visible_attrs.py) alongside the existing content/attr/style ones, so a widget can carry CSS classes built from independent named slots (color, size, shape...) without them overwriting each other. Bouton and Titre get their Bulma base class (button/title) via fixed_attrs; Conteneur gets an opt-in "Carte (Bulma)" preset instead of a forced default, since it's also used as an invisible layout wrapper. Bouton's font-size/border-radius sliders no longer freeze their default value into inline style at creation (new c_slider no_freeze flag), so Bulma's own button styling shows through until a user actually customizes it — inline style still wins over any class the moment it's set, exactly like today.

Loads bulma.min.css via CDN in screen_edit.html and play.html, same pattern as animate.css.
2026-08-24 12:54:50 +02:00
william 26cf245ffe Truncate long cells in the data table and add a row-detail popup
Cells now clip with an ellipsis instead of wrapping and inflating row height. A new eye button before the pencil opens a popup showing every field of that row in full, label above value, for comfortable reading.
2026-08-24 11:33:29 +02:00
william 0e5db97180 Let Texte/Titre elements bind directly to a single row of another object
Adds a "Donnée liée" settings group to Texte/Titre: pick an object, optionally match it to the current game state via 1-2 filter conditions (same engine as the data-repeater's filter, including {{Objet.champ}} cross-references), and use {{champ}} in the text content to show a field from that one matching row. Unlike the data repeater — built for showing a list of rows — this covers displaying a single computed value (e.g. the objective of the level matching the game's current parcours/level) without wrapping it in a repeater.
2026-08-24 11:02:54 +02:00
william f58fb2bc3d Support a second AND condition on the data-repeater filter
Needed to filter a "level" object by both its parcour_id and its number at once (e.g. show the level matching the game's current_parcours AND current_level) — the previous filter only supported a single condition.
2026-08-24 10:48:52 +02:00
william d8a80b8a8a Show existing fields as a scrollable table on the object-edit page
Replaces the stacked per-field cards with a compact table (one row per field) for a clearer overview when an object has many fields. The page itself no longer scrolls — only the table body and the left column do, mirroring the screen editor's fixed-viewport layout. Each row stays independently editable/deletable via hidden forms targeted with the HTML form= attribute.
2026-08-24 10:28:03 +02:00
william 2bb0c253f9 Fix object-form breaking on repeat pjax visits, and stray autofill in game rename field
object_form.js declared top-level const bindings, which pjax replays verbatim on every visit — the second visit threw "already declared" and silently broke "Ajouter un champ"/"Créer l'objet". Wrapped it in an IIFE. Also renamed the generic name="name" rename-game field to name="game_name" with autocomplete off, since browsers were autofilling it with unrelated previously-typed values.
2026-08-24 09:21:19 +02:00
william 4b05301e2e Add drag-and-drop reordering of sibling elements in the tree panel
Elements can now be reordered within the same container by dragging a row above or below another in the left-hand element tree.
2026-08-24 08:43:36 +02:00
williamandClaude Sonnet 5 17fa4cf087 Add an Onglets (Tabs) widget
New widget where each tab is a real "conteneur" element posed as a child
(see screens/elements/add_tab.py) — this reuses everything that already
exists for a normal container (adding a Répéteur/Conteneur/etc. inside via
"Ajouter DANS ce conteneur", renaming to change the tab's visible label,
deleting via the standard trash icon) instead of inventing a separate
storage format for tabs.

The widget's own properties panel gets a dedicated "Onglets" section to
add a tab, rename one, jump to its content, or delete it. Rendering
(render_onglets.py) builds a tab bar + one panel per tab, switched
client-side (forgeShowTab, in both screen_edit.html and play.html) with
only one panel visible at a time.

Distinct from the existing "activer_onglet" flow action (2.3, manual
show-one/hide-siblings) — that stays available for custom show/hide
wiring; this widget is the turnkey version with tab management built in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 21:42:39 +02:00
williamandClaude Sonnet 5 2b2619d1c4 Fix selection highlight getting clipped when flush against a container edge
The positive outline-offset from the previous fix pushed the highlight
outside the element's own box — which gets clipped by the first
overflow:hidden ancestor (the canvas itself, or a rounded-corner
container) on whichever sides touch that ancestor's edge. A row of
gauges flush against the top/left of its container showed only a
stray line on the untouched side instead of a full frame.

Switch to an inset box-shadow: it always stays inside the element's own
box, so it can never be clipped by an ancestor, and it naturally follows
the element's own border-radius.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 20:53:48 +02:00
williamandClaude Sonnet 5 3228460f1c Give the selected-element canvas outline breathing room
A compact element like a Jauge (name label + thin bar, no padding) got
an outline pulled 1px INSIDE its box (outline-offset:-1px), which hugged
the content so tightly it looked like it was slicing through the bar
instead of framing it. Push the outline outward instead (+3px) with a
soft glow, so the selection frame reads clearly regardless of how
tightly the underlying element is sized.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 20:34:48 +02:00
williamandClaude Sonnet 5 d8ab71ddeb Highlight the selected element on the canvas even when it's nested
A top-level element already got a selection border via its
.canvasElement wrapper, but a child posed inside a container/répéteur/
groupe de champs has no such separate frame, so selecting it from the
tree gave no visual feedback on the canvas at all.

applySelectionHighlight() now targets the element's own tag directly via
data-element-id (present on every rendered element, nested or not) and
outlines it, re-run after every canvas refresh path (initial load, full
pjax navigation, partial panel refresh, and the autosave-only canvas
refresh).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 20:25:05 +02:00
williamandClaude Sonnet 5 386e93d9c6 Move element tree above the add-element menus, make containers collapsible
The "Éléments de cet écran" tree now sits above "Ajouter un élément sur
l'écran" / "Mes éléments de jeu" so it's the first thing visible in the
left panel.

Each container row gets a disclosure triangle that collapses/expands its
children, independent from the row's link (which still selects the
element and shows its properties whether collapsed or expanded). The
collapsed state is remembered per element in localStorage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 20:17:21 +02:00
williamandClaude Sonnet 5 bb79f2f93d Redesign the screen editor's element tree and add duplication
The "Éléments de cet écran" tree now renders every level of nesting
(previously stopped after one level of children) as a compact single-line
list, and right-clicking a row opens a context menu to duplicate the
element (and its full subtree) in place, in its current container.

Also: all property panels start collapsed instead of some being open by
default, the redundant nested element list inside "Ajouter DANS ce
conteneur" is removed (it only needs the widget picker), and the
now-unneeded "a container is selected" warning banner is gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:53:38 +02:00
williamandClaude Sonnet 5 3039489e39 Fix Jauge name centering and add name appearance controls
Center the name+bar block vertically within the Jauge's own box
(justify-content:center) so it no longer looks pinned to the top once a
name label adds extra content height.

Add appearance settings for the name: position (above the bar, or beside
it on the left), alignment when above (centered or left-aligned), font
family, font size, bold, and italic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:23:19 +02:00
william 7e4df3fa3a Fix Jauge bar collapsing to 0px when nested with a name label
A Jauge posée dans un conteneur a une hauteur "auto" (voir
_style_string) — "flex:1 1 auto" seul n'avait alors rien à répartir
(le conteneur flex lui-même n'a pas de hauteur définie), donc le
wrapper de la barre s'effondrait à 0px : seul le nom restait visible,
la jauge elle-même disparaissait. Ajout d'un "min-height" plancher
sur ce wrapper, qui laisse toujours la barre visible dans ce cas tout
en la laissant grandir avec flex:1 si l'élément a une vraie hauteur.
2026-08-23 18:55:44 +02:00
william c9d3069f47 Fix Jauge always reading the object's most recent row
A Jauge could only target a whole object, not a specific record —
three gauges pointing at the same "value" field (e.g. Réputation/
Trésorerie/Confiance in one "jauge" object) all silently showed the
most recent row's value, with no way to tell them apart.

Add "Enregistrement (ligne) à suivre" (row_id) so a Jauge targets one
specific row, and "Champ contenant le nom" (champ_nom) to show a
label above the bar — both as dropdowns populated from the object's
actual fields/rows (previously "Champ numérique à afficher" was free
text the user had to type correctly by hand). No regression: without
row_id the widget still falls back to the latest row, exactly as
before.

Extract data_definition_options() (rows+fields for a definition) out
of routes/screens/screen_edit.py so controls_with_values.py can reuse
it server-side for the initial render; a small client-side handler
(bindJaugeDefinitionSelect) repopulates the same selects live when
the tracked object is changed without leaving the panel.

Verified live with Playwright: picking "jauge" then "Confiance" then
"value"/"name" in the panel renders an 80%-filled, green-leaning bar
labelled "Confiance" on /play — not the 20%/50% of the other rows.
2026-08-23 18:02:02 +02:00
william 4eac20a464 Rewire hover as a flow trigger
Add "survol"/"fin_survol" trigger events (mouseenter/mouseleave,
same anti-doublon pattern as bindClicks()) so hovering an element can
run flow nodes, instead of the old static hover-text-only control
removed from the properties panel. Also add "Contenu" as a settable
"Modifier un élément" property so an action can display a defined
text on another element on hover — the concrete use case that
motivated this.

Verified live with Playwright: a "Au survol" trigger on one element
correctly updates another element's text via the action, and text
stays put with no "Fin du survol" wired (explicit, no implicit
revert, consistent with "Au clic").
2026-08-23 17:16:48 +02:00
william 4a593d07e4 Consolidate the properties panel: Position & taille, no more Taille/Survol/duplicate Disposition
- Merge "Taille dans le conteneur" into "Position & taille": a child
  element (posé dans un conteneur/répéteur/groupe de champs) now shows
  greyed-out X/Y (not applicable, it follows its parent's layout) and
  a single Largeur/Hauteur field per axis with a unit selector (%/px)
  instead of the old fixed-px-only sliders capped at 1000 — which was
  the root cause of a landscape-mode bug where a child couldn't be
  made wider than 1000px even though the actual screen was wider.
  New c_size control type (one style key per axis, not two competing
  sliders — an earlier px+% two-slider attempt let the untouched
  slider silently clobber the other's value on every autosave).
- Merge the two "Disposition" groups (visibility + scale, previously
  split apart in UNIVERSAL_CONTROLS by unrelated groups) into one.
- Remove the "Survol" panel: hovering is conceptually a flow trigger,
  not a static element property — to be reintroduced there. The
  underlying data-hover-text/bindHoverTexts runtime is untouched.
2026-08-23 12:14:00 +02:00
william d86b1a60c7 Fix builder canvas resizing on element selection
main.content (.content-builder) inherited "margin:0 auto" from the
base .content class. On this page body is a column flex container, so
that auto margin disabled cross-axis stretch and made the element
shrink-to-fit instead — sized via an algorithm that runs back down
through the flex/%/aspect-ratio chain to the canvas, and shifts
slightly whenever sibling panel content changes (e.g. selecting an
element). Give it an explicit width:100% so max-width/auto-margin
centering still work but sizing is stable and fills the available
space. Also add min-width:0 to .builderPanelRight, matching its
siblings, as defense against the same class of bug.

Verified with Playwright: canvas box was previously not filling
available width (918px expected vs 379px actual at a 1920px viewport)
and shifted between steps; both are fixed.
2026-08-23 11:00:09 +02:00
william 099c725df6 Fix su invocation in docker-entrypoint.sh causing 502 in prod
Build and deploy / build-and-push (push) Successful in 9s
Build and deploy / deploy (push) Successful in 7s
su forge -s /bin/sh -c 'exec "$@"' -- "$@" didn't forward gunicorn's
argv correctly with this image's su, so the container never actually
started gunicorn — Caddy then had nothing to proxy to (502). Flatten
the command into a single string via "$*" instead.
2026-08-23 08:58:54 +02:00
william e94d23b1bf Fix projects/ volume ownership on container start
Build and deploy / build-and-push (push) Successful in 49s
Build and deploy / deploy (push) Successful in 8s
The named Docker volume mounted at /app/projects is created by Docker
as root before the container starts, so the image's build-time chown
never applies to it. A root-owned volume made the app (running as the
non-root forge user) unable to write new game folders in production.

Add an entrypoint that chowns /app/projects to forge at each startup,
then drops privileges before exec'ing gunicorn.
2026-08-23 08:47:10 +02:00