cc89b3f7e2ecc373764b0fa56a610d17d167b259
96
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4585a72588 |
Phase 1 (3/3) : bascule "par joueur" dans le tableau de bord
Dernier morceau du plan d'état par joueur : le créateur choisit, à la création d'une variable globale ou d'un objet, si chaque joueur aura sa propre valeur/ses propres lignes (coché par défaut) ou si elle est explicitement PARTAGÉE par tous les joueurs (ex. un compteur de visiteurs global, un catalogue commun) — voir db/global_vars/create_global_variable.py et db/definitions/create_definition.py (Phase 1, 1er commit). routes/global_vars/create_global_var.py, routes/objects/object_new.py : lisent la case à cocher "per_player" du formulaire (absente => reste per_player=1, comportement par défaut). templates/game_dashboard.html : case à cocher sur les deux panneaux de création + colonne "Par joueur" dans les deux tableaux existants, pour que ce réglage (immuable après création, comme le nom d'une variable) reste visible. db/definitions/list_definitions.py appelait _definitions directement sans jamais migrer son schéma — un tableau de bord ouvert avant la toute première création/modification d'objet aurait affiché "Non — partagé" pour un objet en réalité per_player=1 (colonne absente => Undefined, donc faux en Jinja) : corrigé en appelant ensure_field_bounds_schema() ici aussi, comme le fait déjà create_definition.py/get_definition.py. Vérifié : 239 tests passent (2 nouveaux, dont un qui aurait détecté le bug ci-dessus). Phase 1 (état par joueur) est maintenant complète : couche db/, route publique /jouer/<slug>, et ce réglage créateur. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
3c39f1a249 |
Phase 1 (2/3) : route publique /jouer/<slug> + identité visiteur
Ajoute la vraie route hébergée multi-joueurs qui manquait totalement
(voir le constat d'exploration : /game/<slug>/play est réservé au
créateur connecté, "Publier" ne génère qu'un exécutable mono-joueur) —
un visiteur anonyme peut maintenant jouer un jeu explicitement publié en
ligne, avec sa propre partie (variables/objets per_player posés dans le
commit précédent).
core/player_identity.py : identité visiteur via un cookie NON SIGNÉ
(forge_player_id, secrets.token_urlsafe(16), 1 an) — une simple clé de
partition, jamais un jeton d'autorisation.
routes/public_play/ : 4 routes, miroirs des routes existantes de l'aperçu
créateur mais threadées avec le vrai player_id du cookie au lieu du
sentinel PLAYER_SHARED :
- GET /jouer/<slug> (game_play_public.py)
- GET /jouer/<slug>/runtime-payload
- POST /jouer/<slug>/flow/nodes/<id>/run-data
- POST /jouer/<slug>/flow/nodes/<id>/run-variable
Chacune vérifie elle-même db.is_public_played(slug) (404 sinon) — un jeu
n'est exposé publiquement que si le créateur l'a explicitement basculé
"Publier en ligne" (nouveau db/games/is_public_played.py, réutilise la
table générique _meta, comme game_meta.py pour 'name').
core/auth_guard.py : les 4 endpoints publics ajoutés à _PUBLIC_ENDPOINTS
— la garde générique de connexion les laisse passer sans session, mais
chaque vue vérifie quand même is_public_played elle-même (défense en
profondeur, pas seulement une liste d'exceptions). CSRF (core/csrf_guard.py)
n'a besoin d'AUCUN changement : le jeton est déjà lié à la session Flask,
qui existe pour n'importe quel visiteur (connecté ou non).
templates/play.html : FORGE_PLAY_URLS (posé en Phase -1) ne construit
plus ses URLs via des noms de endpoint fixes (url_for('runtime_payload',
...)) mais reçoit des URLs déjà résolues par la route elle-même
(runtime_payload_url/flow_node_run_data_url/flow_node_run_variable_url)
— nécessaire puisque ce même template sert maintenant DEUX familles de
routes (aperçu créateur ET partie publique), chacune avec ses propres
noms de endpoint. routes/play/game_play.py (aperçu créateur, INCHANGÉ
comportement) et game_play_public.py passent chacun ses propres URLs.
templates/base.html : bascule "🌐 Publier en ligne" dans la barre de
navigation du jeu, à côté de "📦 Publier" (export .zip) — deux
fonctionnalités distinctes. db/games/game_meta.py expose maintenant
is_public_played, disponible partout où `game` est dans le contexte.
Vérifié : 237 tests passent (5 nouveaux dans test_public_play.py, dont un
bout-en-bout via HTTP avec deux VRAIS clients de test anonymes — deux
cookies forge_player_id différents — qui obtiennent des valeurs de
variable indépendantes, et un qui verrouille que l'aperçu créateur reste
inchangé). Syntaxe JS validée sur les deux variantes de play.html rendu
(aperçu créateur et partie publique).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
fe807ba51e |
Phase -1 (suite) : découpe l'éditeur de screen_edit.html en modules JS
Même chantier que le commit précédent (moteur de jeu, play.html) — templates/screen_edit.html était un unique fichier HTML+CSS+JS de 3312 lignes, tout l'éditeur (arborescence, panneaux flottants, canevas, formulaire de propriétés, éditeur de flow à nœuds, blocs de logique, timeline d'animation) vivant dans UN SEUL <script>. Ce fichier est plus imbriqué que play.html : de nombreux appels s'exécutent au niveau racine du script (pas seulement des déclarations de fonctions), et JavaScript hoiste les déclarations `function` sur TOUT le script — un appel au niveau racine peut donc référencer une fonction déclarée PLUS LOIN dans le même fichier. Découper naïvement casserait cet ordre implicite. Un audit dédié (analyse ligne par ligne de chaque appel racine + son graphe d'appel transitif) a identifié 3 références "en avance" réelles, toutes regroupées dans la même zone (initBuilderPanel()/toggleActionFields() → bindAspectButtons/ toggleElementPropertyValue/onDataDefinitionChange) — le découpage respecte cette contrainte : chaque fichier est une TRANCHE SÉQUENTIELLE de l'original (jamais une réorganisation), et cette zone spécifique reste un seul fichier (panel-init.js) pour que le hoisting continue de fonctionner exactement comme avant. 5 fichiers sous static/js/screen_edit/ : - tree-panels.js — arborescence, menu contextuel, panneaux flottants gauche/droite, galerie d'icônes, modale de suppression/choix d'icône, glisser-déposer du canevas, panneau de propriétés (autosave). - panel-init.js — (ré)initialisation du panneau central après chaque changement de sélection, filtres de répéteur/donnée liée, condition de visibilité, champs d'action du formulaire de nœud. - flow-editor.js — éditeur de flow à nœuds (rendu du graphe, formulaire d'ajout de nœud, blocs de logique — currentBlockNodes/Edges). - tabs-and-blocks.js — onglets du centre, panneaux flottants génériques (drag/resize/plein écran), modale d'un bloc de logique. - animation-timeline.js — timeline d'animation (clips Animate.css/ personnalisés). Toutes les données injectées par Jinja (GAME_SLUG, SCREEN_ID, DEFINITIONS_DATA, FLOW_NODES_INITIAL, ELEMENTS_LABELS, CUSTOM_EVENTS_MAP, ANIM_CLIPS...) sont posées UNE FOIS par un petit <script> inline resté dans le template, avant les <script src> — même patron que static/js/play/. Le seul bout de logique resté inline est la toute petite IIFE d'ouverture initiale (?tab=/?block=), qui dépend directement de request.args et doit s'exécuter après que tous les fichiers soient chargés. tests/conftest.py : screen_edit_js_bundle() (même principe que play_js_bundle(), Phase -1 précédente) — 4 tests qui vérifiaient la présence de telle fonction/chaîne dans le HTML de l'éditeur (le JS y était inline) sont mis à jour pour chercher dans ce bundle. Un des deux échecs révélait un test déjà fragile (assert "Ligne cliquée" in html vérifiait en réalité le TEXTE SOURCE d'un <script> inline, jamais du HTML réellement rendu — ce texte ne peut plus s'y trouver une fois la fonction qui le construit dynamiquement déplacée dans un fichier externe) : corrigé pour vérifier le bundle JS + la disponibilité de la route séparément. Vérifié : 215 tests passent, syntaxe JS validée sur les 5 nouveaux fichiers (node --check) et sur les <script> inline restants (rendus via le client de test). Test manuel recommandé (édition complète d'une scène : arborescence, propriétés, glisser-déposer, logique de flow, blocs, timeline) avant de considérer ce découpage définitivement sans risque — comme pour play.html, ce fichier n'a pas de harnais de test DOM automatisé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
dbada333d5 |
Phase -1 : découpe le moteur de play.html en modules JS + premiers tests JS
templates/play.html était un unique fichier HTML+CSS+JS de 1069 lignes,
tout le moteur de jeu vivant dans UN SEUL <script>, sans aucune
couverture de test sur cette logique (seuls le rendu HTML et la syntaxe
JS étaient vérifiés). La feuille de route à venir (état par joueur,
hasard, clavier/minuteur, position/collision, son — voir le plan) va
justement faire grossir ce moteur : "un fichier = une fonction, un
dossier = une responsabilité" s'applique aussi au JS, pas seulement au
Python — le moment de découper est avant d'ajouter encore plus de code,
pas après.
Découpage en 6 fichiers sous static/js/play/, calqués sur les sections
déjà présentes dans le code (aucune réorganisation de logique, une pure
extraction) : screens.js (affichage d'écran, timeline d'animation),
conditions.js (évaluation des conditions — la partie 100% PURE, sans
DOM, la plus testable), actions.js (exécution des actions), triggers.js
(recherche des nœuds déclencheurs, attache des écouteurs), bindings.js
(résolution des {{champ}}, rafraîchissement des données), flow-engine.js
(parcours du graphe, événements personnalisés).
Zéro nouvel outillage : plusieurs <script src> dans l'ordre, partageant
le même espace global qu'avant (aucun bundler, aucune étape de build).
Les 2 URLs de route dont ces fichiers ont besoin (flow_node_run_data/
run_variable, runtime_payload) ne peuvent plus être injectées par Jinja
directement dans le code (un fichier statique n'est jamais passé par le
moteur de templates) — elles sont maintenant posées une fois dans
window.FORGE_PLAY_URLS par le petit <script> inline restant dans
play.html, qui ne porte plus que les données Jinja (gameData) et
l'amorçage (bindClicks() etc. au chargement).
publish/build_package.py : ajoute static/js/play à la liste des fichiers
copiés dans l'exécutable exporté (le mode jouable en dépend désormais).
Premiers tests JS (static/js/play/__tests__/conditions.test.js, lancés
via `node --test`, zéro nouvelle dépendance npm — decision prise avec
l'utilisateur de commencer par la logique PURE seulement, pas par une
couverture DOM via jsdom) : compareValues, resolveVariablePath,
evaluateConditionClause/Node, exactement la logique que les phases à
venir (opérations mathématiques, condition de collision) vont étendre.
tests/conftest.py : nouveau helper play_js_bundle() (concatène tout
static/js/play/*.js) — 13 tests existants qui vérifiaient la présence de
telle fonction/chaîne dans le HTML de /game/<slug>/play (tout le JS y
était inline avant ce découpage) sont mis à jour pour chercher dans ce
bundle à la place ; les tests qui vérifient un CSS/HTML réellement resté
dans play.html (forgeHighlight, forgeDisabled, #playFrame...) continuent
de chercher dans le HTML.
Vérifié : 215 tests pytest passent (aucune régression comportementale,
juste une réorganisation), 13 tests node:test passent, node --check sur
chacun des 6 nouveaux fichiers. Test manuel recommandé (jeu joué de bout
en bout : navigation, clic, survol, répéteur, condition, animation)
avant de considérer le découpage définitivement sans risque.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
a445b72a6e |
Corrige "Ouvrir"/"Modifier" inertes dans l'onglet Blocs de logique
Deux bugs distincts, signalés par capture d'écran (bouton "Ouvrir" sans
effet, ligne d'édition affichée en permanence au lieu d'être cachée) :
1. onclick="openLogicBlockPanel({{ b.id }}, {{ b.name|tojson }})" cassait
l'attribut HTML : tojson produit des guillemets DOUBLES (valides en
JSON), qui terminaient prématurément l'attribut onclick="..." lui-même
entre guillemets doubles — le gestionnaire de clic généré était donc
tronqué et invalide, provoquant une erreur JS non interceptée qui
arrêtait aussi tout le script restant dans la même balise <script>
(dont l'IIFE qui devait poser window.openLogicBlockPanel). Corrigé en
ne passant que l'id dans l'attribut et en retrouvant le nom du bloc
côté client depuis FLOW_BLOCKS (déjà chargé) — plus aucune chaîne
utilisateur à échapper dans un attribut HTML.
2. <tr class="hidden" id="blockEditRow..."> ne se cachait jamais : le
CSS ne définissait .hidden que scopé (.floatPanel.hidden,
.columnFilterMenu.hidden), jamais en règle générique — ajoutée dans
styles/forge-custom.css.
Vérifié : 215 tests passent, syntaxe JS validée sur un scénario avec un
vrai bloc existant (reproduisant exactement la situation signalée),
onclick généré inspecté directement dans le HTML rendu.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
1b7706b357 |
Ajoute les blocs de logique : organise le graphe de flow en sous-graphes nommés
Le graphe de logique d'une scène s'affichait jusqu'ici sur un seul canevas plat (toutes les scènes accumulant leurs nœuds sur la même grille), ce qui ne tient pas à l'échelle dès qu'une scène évolue au fil de l'avancée du joueur et accumule des centaines/milliers de nœuds. Ajoute les "Blocs de logique" : un bloc regroupe un sous-ensemble de nœuds/arêtes d'un écran sous un nom et une description (comme une fonction). L'onglet "Logique de la scène" devient une liste de blocs (nom, description tronquée à 3 phrases, éléments concernés, nombre de nœuds, bouton "Ouvrir"). Ouvrir un bloc affiche SON graphe dans une modale plein écran, redimensionnable et déplaçable (patron déjà mûr dans game_dashboard.html, porté tel quel : makeFloatPanelDraggable/ Resizable/Fullscreenable). Décision d'architecture : un bloc est un automate FERMÉ — impossible de relier un nœud d'un bloc à un nœud d'un autre bloc (rejeté côté serveur dans flow_edge_add.py). Toute communication entre deux blocs passe par le système d'événements personnalisés déjà en place (declencher_evenement / trigger_event="evenement"). Détails techniques : - Nouvelle colonne _flow_nodes.block_id (nullable, sans FK — même rationale que trigger_element_id/target_element_id, voir screens/elements/delete_element.py) et nouvelle table _flow_blocks (screens/flow/ensure_flow_schema.py, screens/flow/blocks/ensure_flow_blocks_schema.py). - Migration douce et automatique : les nœuds posés avant l'existence des blocs (block_id NULL) sont rattachés, à la première ouverture de l'onglet, à un "Bloc principal" auto-créé (screens/flow/blocks/ list_flow_blocks.py) — aucun script de migration séparé, aucune donnée perdue. - Suppression d'un bloc = cascade complète (bloc + tous ses nœuds/ arêtes), patron identique à screens/custom_events/delete_custom_event.py mais scopé à un seul bloc plutôt que game-wide. - Routes CRUD sous routes/flow_blocks/, montées comme routes/custom_events/. - templates/screen_edit.html : FLOW (global unique) renommé en ALL_FLOW (toutes les données de l'écran) ; un seul bloc ouvert à la fois (modale unique, à la Unity) — currentBlockNodes()/currentBlockEdges() filtrent ALL_FLOW par CURRENT_BLOCK_ID à chaque rendu, sans tenir de seconde copie à synchroniser manuellement. Vérifié : 215 tests passent (7 nouveaux dans tests/test_flow_blocks.py, dont un qui verrouille l'ordre d'appel list_flow_blocks()/ list_flow_nodes() dans screen_edit.py — la migration douce doit tourner AVANT le chargement des nœuds, sinon le compte de nœuds affiché juste après une migration est périmé), syntaxe JS validée (script de screen_edit.html rendu via le client de test puis node --check). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a7a315cce7 |
Corrige un bug moteur : plusieurs déclencheurs "Au clic" (ou survol) sur le même élément n'exécutaient que le premier
Diagnostic effectué directement sur projects/test/game.db de
l'utilisateur (suite à son signalement "je ne parviens pas à mettre fin
à la surbrillance") : deux nœuds Déclencheur distincts ("Au clic",
id=4 et id=42) référençaient le même trigger_element_id=65. La fonction
findTriggerNode() utilisait Array.find(), qui ne retourne que la
PREMIÈRE correspondance — le second nœud (celui qui devait couper la
surbrillance) n'était donc jamais exécuté, silencieusement, quel que
soit le graphe construit dans l'éditeur.
Ce n'est pas un bug lié aux événements personnalisés ni aux conditions
par variable (deux pistes explorées avant ce diagnostic) : c'est une
limitation générale du moteur, qui n'a jamais géré plus d'un déclencheur
"Au clic"/"Au survol"/"À la fin du survol" par élément.
Renomme findTriggerNode() en findTriggerNodes() (pluriel) : retourne
désormais TOUS les nœuds correspondants (élément + type d'événement),
et bindClicks()/bindHoverTriggers() exécutent chaque flow trouvé au lieu
de s'arrêter au premier.
Vérifié : 208 tests passent, syntaxe JS validée (script de play.html
rendu via le client de test puis node --check). Comme pour le reste du
graphe de logique côté client, ce changement n'est pas couvert par les
tests automatisés (pas de harnais navigateur/DOM) — vérification
manuelle recommandée sur le scénario réel de l'utilisateur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
2c59e54556 |
Simplifie les événements : notification pure, sans paramètre
Retour de l'utilisateur sur le premier jet : "Déclencher un événement" ne doit JAMAIS faire choisir un élément — c'est une notification pure, rien de plus. C'est à l'ÉCOUTEUR (déclencheur "Sur un événement personnalisé" → condition → action) de décider quoi faire ensuite, avec ses réglages habituels (cible fixe, "Ligne cliquée"...), jamais à l'événement de transporter un paramètre. Retire donc tout le mécanisme de transmission ajouté au tour précédent (has_element_param, target_element_from_event, EVENT_ROW_ID, window.lastEventParams) : - db/custom_events/ : _custom_events perd sa colonne has_element_param — un événement n'est plus qu'un nom + une description. - screens/flow/ : retire target_element_from_event (colonne ajoutée par ALTER TABLE, laissée inerte sur les bases déjà migrées — sans conséquence, plus jamais lue ni écrite) et la constante EVENT_ROW_ID. - routes/flow/flow_node_run_data.py : retire la résolution EVENT_ROW_ID, revient à sa forme d'origine (seul CLICKED_ROW_ID reste géré). - templates/screen_edit.html : le nœud Action "Déclencher un événement" n'a plus qu'un sélecteur d'événement — plus de champs élément/ligne. Le nœud Action "Modifier un élément" perd la case "Utiliser l'élément transmis par l'événement en cours". L'onglet Événements perd la case à cocher "Paramètre" (création et édition). - templates/play.html : window.dispatchGameEvent(eventId) ne prend plus que l'id de l'événement — scan global inchangé, mais ne pose plus aucun window.lastEventParams. modifier_element et readFieldValue reviennent à leur résolution d'origine (plus de branche event-aware). 208 tests au total (2 tests retirés, devenus sans objet : la persistance de target_element_from_event et la résolution serveur d'EVENT_ROW_ID). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
666aa892e0 |
Corrige le ciblage élément+ligne transmis par un événement dans un Répéteur
Signalé par l'utilisateur : quand "Modifier un élément" utilise la case
"Utiliser l'élément transmis par l'événement en cours"
(target_element_from_event), et que cet élément vit à l'intérieur d'un
Répéteur, la résolution ne ciblait jamais que le PREMIER élément
correspondant trouvé dans toute la page — jamais forcément la bonne
ligne.
Vérifié directement sur un rendu réel : chaque ligne d'un Répéteur
rejoue le MÊME modèle (voir render_repeater.py), donc le MÊME
data-element-id se répète à l'identique sur CHAQUE .repeaterItem — seul
data-row-id (posé sur l'enveloppe .repeaterItem) distingue réellement
une ligne d'une autre. Un simple
document.querySelector('[data-element-id]') global tombe donc toujours
sur la première ligne rencontrée dans le DOM, sans rapport avec la ligne
réellement transmise par l'événement (window.lastEventParams.row_id).
Corrigé : quand l'événement transmet aussi une ligne, la recherche est
désormais scopée à l'intérieur du .repeaterItem[data-row-id=...]
correspondant avant d'y chercher l'élément — sinon (élément fixe, ou
événement sans paramètre de ligne), le comportement global d'avant reste
inchangé.
210 tests toujours verts (ce correctif est purement côté client, jamais
couvert par les tests automatisés — vérifié manuellement via un rendu
réel confirmant la structure .repeaterItem/data-row-id décrite
ci-dessus).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
f436190f90 |
Ajoute les événements personnalisés (éditeur + exécution)
Deuxième moitié de la fonctionnalité "événements" (voir le commit précédent pour le backend) : un nouvel onglet "📣 Événements" dans screen_edit.html (scène ET modèle, même route) pour créer/modifier/ supprimer un événement (nom, description, "a un paramètre élément" oui/ non) et voir où il est déjà écouté/déclenché ; deux nouveaux types de nœud dans le graphe de logique ; exécution réelle côté client (play.html). - templates/screen_edit.html : - Onglet Événements : barre de création + tableau (patron form/name="..." + attribut `form="eventEditFormN"` déjà utilisé par l'onglet Variables du tableau de bord, pour éditer plusieurs champs d'une ligne sans emboîter un <form> dans un <tr>). Colonne "Utilisé par" avec liens directs vers les écrans/modèles concernés (list_custom_event_usages, déjà en place côté backend). - Nœud Déclencheur "Sur un événement personnalisé" : un simple sélecteur d'événement (trigger_custom_event_id) — ni élément ni écran, retrouvé par un scan global côté client (comme "À l'affichage de l'écran"). - Nœud Action "Déclencher un événement" : sélecteur d'événement (target_custom_event_id), avec élément/ligne concernés affichés seulement si l'événement a has_element_param (réutilise le sélecteur d'élément existant + "Ligne cliquée" pour la ligne, pas de définition d'objet ici donc pas de liste de lignes fixes possible). - Nœud Action "Modifier un élément" : nouvelle case "Utiliser l'élément transmis par l'événement en cours" (target_element_from_event) — v1, seule cette action l'expose (extensible plus tard sans nouveau changement de schéma). - nodeLabel()/submitNodeForm()/toggleFlowTriggerFields()/ toggleFlowActionFields() étendus en conséquence, CUSTOM_EVENTS_MAP (id -> nom/has_element_param) exposé côté JS pour le rendu des libellés et l'affichage conditionnel des champs paramètre. - templates/play.html : - window.dispatchGameEvent(eventId, elementId, rowId) : scan de gameData.flows (toutes scènes ET modèles à la fois, même principe que findTriggerNode()/runScreenShowTriggers()) pour trouver chaque écouteur, pose window.lastEventParams puis exécute son graphe (runFlowFrom) — au même titre que window.lastClickedRowId pour "Ligne cliquée". - runActionNode : nouvelle branche "declencher_evenement" (résout CLICKED_ROW_ID pour la ligne transmise, comme "Modifier une donnée") ; branche "modifier_element" étendue pour résoudre dynamiquement l'élément depuis window.lastEventParams quand target_element_from_event est actif. - readFieldValue (évaluation de condition) et l'appel serveur de "Modifier une donnée" (routes/flow/flow_node_run_data.py) résolvent désormais aussi EVENT_ROW_ID (-2), au même endroit que CLICKED_ROW_ID (-1) déjà en place. - routes/screens/screen_edit.py : passe custom_events/ custom_event_usages/custom_events_map_json au template (même patron que global_variables déjà threadé pour le sélecteur de variable dans le formulaire de Condition). Nouveaux tests dans tests/test_custom_events.py : forme exacte du payload runtime exposé au JS (types entiers, pas des chaînes — une comparaison stricte "===" échouerait silencieusement sinon), target_element_from_event bien persisté, résolution serveur d' EVENT_ROW_ID depuis le corps de la requête. 210 tests au total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
1289079da5 |
Ajoute "Publier" : exporte un jeu en exécutable Windows autonome
Fonctionnalité mise de côté depuis le tout début de ce chantier
("jouable en toute autonomie"). Nouveau bouton "📦 Publier" dans la barre
de navigation du jeu (après "▶️ Jouer") : construit un zip contenant un
Python portable + Flask embarqués, une copie figée du moteur de rendu
(screens/db/filters + le strict minimum de core/) et des données du jeu
(game.db + uploads/), lancé en double-cliquant sur run.bat — aucune
installation requise, 100% hors-ligne (voir le commit précédent qui a
auto-hébergé polices/animate.css, dernière dépendance CDN de l'app).
Exploration préalable a confirmé que screens/ et db/ sont déjà
totalement découplés de auth/routes/core (seul lien : un try/except
optionnel dans db/connection.py) et que templates/play.html n'appelle
que 4 endpoints "purs" (aucune logique d'auth mélangée dedans) — ce qui
a permis de réutiliser ces routes quasiment telles quelles dans un
mini-serveur Flask séparé plutôt que de les réécrire.
- db/constants.py : PROJECTS_DIR devient surchargeable via
FORGE_PROJECTS_DIR (même schéma que auth/connection.py) — le serveur
joueur autonome pointe ainsi vers son propre dossier "projects/"
embarqué.
- publish/vendor_runtime.py : télécharge (une fois par poste, mis en
cache sous data/publish_vendor/ — déjà ignoré par git) le ZIP Python
embeddable officiel (python.org) et vendore Flask via pip --target ;
fonctions séparées et mockables pour ne jamais déclencher de vrai
téléchargement dans les tests.
- publish/player_app_template.py : mini-Flask autonome, réutilise
core/flask_app.py et core/jinja_filters.py tels quels (SLUG figé en
dur au moment de la publication, csrf_token() factice puisqu'aucune
session n'existe dans cet export). sys.path doit être complété
manuellement au démarrage : le python311._pth de la distribution
embeddable ne référence que le dossier de python.exe lui-même, jamais
celui du script lancé.
- publish/build_package.py : assemble le zip dans un dossier temporaire
(jamais les vrais fichiers de l'app), copié/nettoyé après envoi de la
réponse HTTP (routes/publish/publish_game.py, déjà protégée par la
garde d'accès existante — aucune vérification supplémentaire).
- templates/base.html : bouton + modale (avancement séquentiel, jamais
de suivi serveur réel — le build est rapide) qui déclenche le
téléchargement du zip via un blob, comme la modale des codes de
récupération déjà en place (posée À L'INTÉRIEUR de <main> pour que
pjax.js la remplace et rejoue son script à chaque navigation).
Vérifié pour de vrai (pas seulement via les tests) : zip construit,
extrait, lancé avec le Python embeddable réel — /, /game/test/play,
/game/test/runtime-payload et /static/style.css répondent tous 200.
tests/test_publish.py (nouveau, vendor mocké) : structure du zip,
SLUG correctement substitué, route protégée par la même isolation par
projet que le reste de l'app. 203 tests au total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
5d6747770c |
Auto-héberge les polices Google Fonts et animate.css (fin des CDN)
Étape 1 de la fonctionnalité "Publier un jeu en exécutable autonome" : un jeu exporté devra fonctionner sans AUCUNE connexion internet, ce qui suppose d'abord que l'app elle-même n'ait plus aucune dépendance CDN — Bulma l'était déjà (refonte design system), il ne restait que Google Fonts et animate.css, chargés par templates/play.html ET templates/screen_edit.html (aperçu du canevas dans l'éditeur). GOOGLE_FONTS_LINK (screens/widgets/font_options.py) était une constante FIXE (4 polices, 2 graisses chacune, jamais dépendante du jeu en cours) : téléchargées une fois pour toutes (45 fichiers woff2, ~1.1 Mo au total avec la couverture cyrillique/vietnamienne incluse) dans static/vendor/fonts/, avec un fonts.css régénéré à partir du CSS officiel de Google Fonts mais pointant vers les fichiers locaux. Même chose pour animate.min.css 4.1.1 (static/vendor/animate.min.css). routes/play/game_play.py et routes/screens/screen_edit.py ne passent plus google_fonts_link aux templates (devenu inutile, les deux pages chargent directement les fichiers locaux) ; GOOGLE_FONTS_LINK est retiré de screens/widgets/font_options.py et screens/__init__.py (plus aucun appelant). tests/test_animations.py : les deux tests qui vérifiaient la présence du lien CDN vérifient maintenant la présence du fichier local (animate.min.css) — renommés en conséquence. 199 tests toujours verts. Reste à faire pour la fonctionnalité complète : empaquetage Python portable + serveur minimal + bouton "Publier". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
6b9a491387 |
Corrige l'enveloppe ouverte/fermée affichées en même temps (bug préexistant)
Ce bug n'est PAS lié à la refonte design system de cette session — le
fichier concerné (templates/play.html) n'avait plus été touché depuis
une session précédente (commit
|
||
|
|
eaebc555d3 |
Refonte design system — Phase D : éditeur de scène et aperçu jouable
L'essentiel du re-skin de l'éditeur de scène (onglets, panneaux flottants, groupes de propriétés, boutons, galerie d'icônes, menu contextuel, modales) était déjà obtenu automatiquement par le passage global des tokens en Phase A — tout ce CSS custom consommait déjà les mêmes variables (--panel/--border/--accent...) que le reste de l'app. Reste ce sweep ciblé des dernières couleurs codées en dur qui échappaient aux tokens : - screen_edit.html : bandeau "élément de jeu réutilisable" (fond/bordure panneau en dur), avertissement de suppression de nœud de logique (fallback --danger périmé), séparateur de clause de condition (gris clair #ccc, incohérent sur un thème exclusivement sombre) — tous reliés à var(--panel)/var(--border)/var(--danger). Les couleurs de swatch par défaut des actions "changer la couleur d'un élément" (#5b8cff, #ff0000) sont volontairement laissées telles quelles : ce sont des valeurs de CONTENU (le jeu du client), pas du chrome Forge. - play.html : #emptyState (message "aucun écran" généré par le moteur, pas du contenu du jeu) relié à var(--forge-text-muted). Le rendu du jeu lui-même (fond du cadre, bordure des champs de saisie posés par le client sur ses écrans) reste strictement inchangé, conformément à la distinction actée avec l'utilisateur entre chrome de l'outil et contenu du jeu créé. - Aucun changement à la disposition (canvas, floatPanel, builder3) — conforme à l'exemption du §5/§9 du document de règles. 197 tests toujours verts ; JS de screen_edit.html (très long) revérifié via une extraction jetable + node --check, comme pour les phases précédentes. Termine la refonte design system en 4 phases (voir regles/FORGE_ENGINE_TEMPLATE_BULMA.md) : Bulma auto-hébergé compilé via Sass avec la palette Forge, chrome global et pages d'authentification, en-têtes de page des écrans de gestion, éditeur de scène et aperçu jouable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
9157b16038 |
Refonte design system — Phase C : en-tête de page (accueil, tableau de bord)
- index.html ("Mes jeux") : le <h1> nu est remplacé par l'en-tête de page
compact du §5 (.dashPanelHeader, déjà utilisé ailleurs pour ce
patron) — titre + badge du nombre de jeux (.forge-badge) sur une seule
ligne. Pas de second CTA : le formulaire de création reste dans sa
colonne dédiée, un bouton de plus ferait doublon.
- game_dashboard.html : ajout du même en-tête, absent jusqu'ici (la page
démarrait directement sur la barre d'onglets) — nom du jeu + chemin du
dossier, pour savoir sur quel jeu on se trouve sans avoir à regarder
l'URL.
- styles/forge-custom.scss : .content-objectEdit > h1/.hint/form/
.warningBanner (règle qui fige la hauteur des enfants directs non
extensibles dans la mise en page flex de l'accueil) étendue à
.dashPanelHeader, qui remplace maintenant le <h1> direct.
- profile.html : déjà conforme (rayons de boîte/bouton, danger tokenisé)
depuis le passage global des tokens en Phase A — aucun changement
supplémentaire nécessaire.
197 tests toujours verts ; vérification ciblée (fixture jetable, supprimée
ensuite) confirmant l'en-tête et le JS du tableau de bord.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
62d25dbdce |
Refonte design system — Phase B : chrome global et pages d'authentification
- styles/forge-custom.scss : .content-wide (déjà utilisée par game_dashboard.html/index.html) passe de 1080px à 1600px, pleine largeur pour les écrans de gestion (§5) — la largeur du bloc <main> par défaut (760px, hérité par les pages d'auth/profil qui ne posent pas cette classe) reste inchangée, ces pages veulent justement rester étroites et centrées. - Pages d'authentification (login/register/2FA×2/mot de passe oublié/ réinitialisation) : le style="max-width:NNNpx" répété sur chacune est remplacé par les classes partagées .authScreen/.authCard(-wide), titre H1 en dégradé signature .forge-gradient-title — seul endroit du site, avec le logo, autorisé à l'utiliser (§3/§5). Grille de fond fine sur body.authBody, elle aussi réservée aux zones hero et absente des écrans de travail. - Logique JS inchangée (jauge de mot de passe dans register.html/ reset_password.html) — uniquement des classes/structure autour. 197 tests toujours verts, JS des pages modifiées revérifié (node --check). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
f2a73f2c73 |
Refonte design system — Phase A : Bulma auto-hébergé compilé via Sass
Met en place les fondations du template Bulma décrit dans regles/FORGE_ENGINE_TEMPLATE_BULMA.md : palette orange/ambre Forge (--accent:#ff5f2e/--accent-2:#ffb020), typographie, rayons — appliqués en recompilant Bulma lui-même plutôt qu'en le surchargeant après coup en CSS, pour recolorer automatiquement ses composants internes (tags, notifications, dropdowns...) sans avoir à les surcharger un par un. - Bulma 0.9.4 vendoré en local (styles/bulma/, source Sass classique $variable + @import) — PAS 1.0.2 (la version jusqu'ici en CDN) : 1.0.2 utilise le nouveau système de modules @use/@forward, que libsass (choisi pour rester 100% Python, sans Node/npm) ne sait pas compiler (testé : il ignore silencieusement le @use au lieu de le traiter). 0.9.4 est la dernière version compatible avec libsass et couvre à l'identique tous les composants utilisés ici (boutons, tableaux, onglets, modales, formulaires, navbar). - requirements.txt : +libsass (pip pur, aucun binaire/Node.js). - styles/forge-theme.scss (nouveau, point d'entrée) : variables Sass Bulma ($primary, $radius...) posées avant l'import, tokens Forge exposés en :root (--forge-bg, --accent, --gradient, --status-*...) avec des alias vers les noms de variables déjà utilisés par tout le CSS custom existant (--bg/--panel/--border/--text/--danger...) — pas besoin de renommer les ~600 lignes de règles déjà écrites, seules leurs VALEURS changent. - styles/forge-custom.scss : ancien static/style.css, structurellement inchangé — seuls les hex/rgba en dur qui échappaient aux variables (ancien accent bleu #5b8cff, danger #e2685f, couleurs de types de nœuds du graphe de logique, fond du QR code recovery...) sont remplacés par les tokens de la charte. - build_css.py (nouveau) : compile styles/forge-theme.scss en static/style.css via libsass — un seul fichier, un seul <link> inchangé dans les templates, à relancer manuellement après toute modification sous styles/. - base.html/play.html : suppression du <link> CDN Bulma (auto-hébergé désormais), ajout d'un favicon (absent jusqu'ici) et du vrai logo Forge dans la navbar (assets/*.svg copiés dans static/branding/, seul dossier réellement servi par Flask). 197 tests toujours verts (aucune assertion sur des valeurs CSS). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
dedbda422c |
Rend la page de profil compacte et sur 2 colonnes
Les 5 sections (email, informations, mot de passe, codes de récupération, suppression du compte) tenaient les unes sous les autres dans une seule colonne étroite (560px) avec le padding/espacement par défaut de Bulma — imposait de scroller pour tout voir. Réorganisé en grille CSS 2 colonnes (container élargi à 980px, static/style.css : .profileGrid/.profileCol/ .profileBox) : compte/infos/suppression à gauche, sécurité (mot de passe, codes de récupération) à droite. Champs resserrés (labels remplacés par des placeholders, is-small partout, marges réduites), jauge de force du mot de passe sur une ligne (.passwordChecklist-compact). Repasse en une seule colonne sous 720px de large. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
c8c949c430 |
Ajoute la régénération des codes de récupération et le changement d'email
Depuis /profile : - "Codes de récupération 2FA" : régénère les 10 codes (invalide les 10 précédents d'un coup, voir auth.generate_recovery_codes) et les affiche une seule fois via le même modal qu'à l'inscription (session flash, core/recovery_codes_flash.py) — mot de passe actuel requis. - "Adresse email" : change l'adresse (mêmes règles qu'à l'inscription — format valide, unicité — voir auth/email_validation.py, désormais partagé avec create_user.py au lieu d'être dupliqué). Pour un compte "user", le dossier de son unique projet porte le nom de son adresse (project_slug = slugify(email)) : il est renommé pour suivre le changement (db/games/move_game.py, même logique d'unicité par suffixe que create_game()), sans quoi project_slug ne correspondrait plus à aucun dossier réel. Un "admin" n'a pas de project_slug dédié : rien n'est renommé pour ce rôle. 18 nouveaux tests (tests/test_profile.py) : mauvais mot de passe refusé pour les deux actions, mauvais format/email déjà pris refusés, dossier bien renommé et project_slug mis à jour, connexion possible avec la nouvelle adresse, anciens codes de récupération bien invalidés après régénération, modal jamais réaffiché deux fois. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
5c2d7c49cd |
Corrige l'affichage des codes de récupération 2FA (invisibles en pratique)
Le modal était placé en dehors de #pageChrome ET de <main> — or pjax.js (swapDocument()) ne remplace jamais que ces deux conteneurs à chaque navigation, jamais le body entier. La redirection qui suit la confirmation de la 2FA passe par une navigation pjax (fetch), pas un vrai rechargement de page : le HTML du modal était bien renvoyé par le serveur, mais jamais copié dans le DOM réellement affiché, donc jamais visible pour un utilisateur qui vient de s'inscrire. Déplacé à l'intérieur de <main class="content"> : fait maintenant partie de innerHTML remplacé par pjax.js à chaque navigation, comme le reste du contenu de page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0d46abe193 |
Ajoute une page de profil (modifier ses infos, mot de passe, supprimer son compte)
Le nom affiché dans la barre de navigation (base.html) devient un lien vers /profile : informations du compte (email en lecture seule, nom/ prénom modifiables), changement de mot de passe (mot de passe actuel requis + mêmes règles de force qu'à l'inscription/la réinitialisation), et une section "danger" pour demander la suppression du compte. La suppression exige le mot de passe actuel ET la saisie exacte de "SUPPRIMER" (deux confirmations distinctes pour une action irréversible) — routes/auth/profile.py. Pour un compte "user" (limité à un seul projet, créé automatiquement et impossible à supprimer autrement, voir core/auth_guard.py), supprimer le compte supprime aussi son unique projet sur le disque, faute de quoi il resterait orphelin sans plus aucun propriétaire. Un compte "admin" peut posséder plusieurs jeux qui ne lui sont pas dédiés de la même façon : ses projets ne sont jamais touchés. L'unique compte administrateur ne peut pas être supprimé (auth/ count_admins.py) — le supprimer bloquerait la création d'un nouveau compte admin (réservée au tout premier compte jamais créé, base vide). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
d5a84413d1 |
Ajoute la réinitialisation de mot de passe par email (SMTP)
Un lien "Mot de passe oublié ?" (login.html) mène à /forgot-password : si l'adresse saisie correspond à un compte, un jeton à haute entropie (secrets.token_urlsafe, valable 1h) est généré et envoyé par email via smtplib (auth/send_email.py, aucune dépendance ajoutée) — configuré uniquement par variables d'environnement (SMTP_HOST/PORT/USER/PASSWORD/ FROM, voir .env.example et docker-compose.prod.yml), n'importe quel serveur SMTP existant convient (Mailcow compris). Seul le hash SHA-256 du jeton est stocké (auth/password_reset.py, table _password_reset_tokens) : un jeton envoyé par email reste inutilisable même en cas de fuite de la base. Le même message générique s'affiche que l'adresse corresponde à un compte ou non, pour ne jamais permettre à ce formulaire de servir à deviner quelles adresses sont déjà inscrites. Un échec d'envoi (SMTP non configuré) est journalisé côté serveur seulement, jamais révélé à l'utilisateur. /reset-password/<token> vérifie le jeton (non expiré, non déjà utilisé), applique les mêmes règles de mot de passe fort qu'à l'inscription (même schéma visuel), puis consomme le jeton et remet à zéro le compteur anti-bruteforce du compte (auth/set_password.py) — une identité prouvée par email est une voie de récupération légitime même pour un compte verrouillé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
9efe119936 |
Ajoute des codes de récupération 2FA (perte du téléphone)
10 codes à usage unique (format "xxxx-xxxx-xxxx") sont générés à
l'instant même où la 2FA est confirmée (auth/recovery_codes.py, table
_recovery_codes séparée pour marquer/consommer chaque code un par un) —
seul leur hash (werkzeug, comme les mots de passe) est stocké, ils ne
sont visibles en clair qu'à cet instant précis.
Plutôt que d'interrompre la redirection habituelle après confirmation de
la 2FA, les codes sont posés en session ("recovery_codes_to_show") et
affichés une seule fois, en modal, dès le premier rendu de base.html qui
suit (core/recovery_codes_flash.py, session.pop) — préserve tel quel le
comportement de redirection déjà couvert par les tests existants.
Sur /login/2fa, un code de récupération est accepté à la place du code
TOTP habituel (routes/auth/login_2fa.py) : verify_totp est essayé en
premier (verify_recovery_code consomme le code dès qu'il correspond, on
ne veut pas en griller un pour rien sur une saisie qui aurait en fait
été un TOTP valide). Compte toujours vers le même compteur anti-bruteforce
que le code TOTP (déjà en place, voir auth/rate_limit.py).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
a52b244f27 |
Ajoute la protection CSRF sur tous les formulaires et requêtes AJAX
Un jeton unique par session (core/csrf.py, exposé côté Jinja via csrf_token()) est vérifié sur toute requête non-GET par un before_request (core/csrf_guard.py), dans le même esprit que core/auth_guard.py : une seule garde globale plutôt que de toucher aux ~90 routes existantes une par une. L'app entière fait déjà transiter ses formulaires par fetch() : pjax.js intercepte chaque <form> interne et le transforme lui-même en requête fetch (aucun usage de l'attribut d'échappement data-no-pjax nulle part dans le repo, confirmé par grep). Il suffit donc de patcher window.fetch UNE SEULE FOIS (static/csrf_fetch.js) pour y ajouter automatiquement l'en-tête X-CSRFToken sur toute requête non-GET, formulaires pjax comme fetch() écrits à la main dans screen_edit.html/game_dashboard.html/ play.html — sans modifier un seul appel existant. La vérification est désactivée quand app.config["TESTING"] est actif (même convention que Flask-WTF/WTF_CSRF_ENABLED), pour ne pas avoir à ajouter le jeton aux ~170 tests existants qui appellent les routes directement via le client de test Flask. tests/test_csrf.py réactive volontairement la garde pour la mettre à l'épreuve pour de vrai (GET jamais bloqué, POST sans jeton/avec mauvais jeton -> 400, POST avec le bon jeton via l'en-tête ou le champ de formulaire -> succès). templates/play.html reçoit les mêmes deux balises que base.html car il est autonome (ne l'étend pas, propre <html>/<head>). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
d90abc827b |
Ajoute l'authentification : inscription, mot de passe fort, 2FA obligatoire, isolation par utilisateur
Première des deux grandes fonctionnalités demandées (authentification d'abord, export HTML/CSS/JS autonome ensuite) : - Inscription (nom, prénom, email UNIQUE, mot de passe) avec schéma visuel du mot de passe (jauge + liste de critères qui passent au vert en direct — auth/password_strength.py, mêmes règles vérifiées côté serveur qu'affichées côté client). - Double authentification (TOTP, compatible Google Authenticator/Authy) OBLIGATOIRE dès l'inscription : QR code (SVG, sans dépendance Pillow) à scanner puis code à confirmer avant que le compte soit utilisable — voir auth/create_user.py (totp_confirmed) et routes/auth/register_2fa.py. - Connexion en 2 temps (mot de passe puis code TOTP), déconnexion. - Isolation par utilisateur : un compte "user" est limité à un SEUL projet, dont le dossier est nommé d'après son adresse email (slugifiée) et créé automatiquement dès la 2FA confirmée — aucune page de gestion multi-jeux pour lui (redirigé directement vers son propre tableau de bord). Le rôle "admin" reste illimité, comme le moteur l'a toujours été (le TOUT PREMIER compte jamais créé sur une base de comptes vide devient automatiquement admin — voir auth/is_first_user.py — pas de mot de passe par défaut à faire circuler : s'inscrire en premier suffit). Un compte "user" ne peut pas non plus supprimer son unique projet (aucune façon d'en recréer un ensuite). - Garde d'accès globale (core/auth_guard.py, un seul before_request) : toute page exige une connexion, sans avoir touché individuellement aux ~80 routes déjà existantes du moteur. tests/conftest.py isole complètement les tests de la vraie base de comptes (FORGE_USERS_DB_PATH/FORGE_SECRET_KEY_PATH vers un dossier temporaire propre à la session de tests) et authentifie automatiquement la fixture `client` partagée en tant que compte admin de test — les 155 tests déjà existants continuent de passer SANS AUCUNE modification de leur côté, exactement comme avant l'authentification. 10 nouveaux tests dédiés (tests/test_auth.py) : inscription/mots de passe/2FA/connexion/ isolation par projet/blocage de suppression, vérifiés en conditions réelles (vrai client de test Flask, vraie base SQLite, vrais codes TOTP calculés avec pyotp). 165 tests au total, tous au vert. Nouvelles dépendances : pyotp, qrcode (requirements.txt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
ed4dd78dad |
Corrige le clignotement de la boîte de dialogue entière au lieu du seul texte
Suite du correctif précédent (
|
||
|
|
124f250d2b |
Corrige le contenu de "Donnée liée" figé après un changement de variable/donnée
Bug rapporté : changer une variable globale utilisée comme valeur de comparaison d'une "Donnée liée" (Texte/Titre, dans une boîte de dialogue posée comme élément de jeu réutilisable) recalculait bien, côté SERVEUR, la bonne ligne à afficher — mais le contenu affiché en jeu restait figé sur son ancienne ligne tant que la page n'était pas complètement rechargée. Cause : templates/play.html ne régénère, après une action "Modifier une variable"/"Modifier une donnée", QUE les éléments dont le rendered_html porte un marqueur connu (voir refreshRuntimeData()/hasMarker() — "repeaterItem", "jaugeBar", "visibilityGated"). Un Texte/Titre "Donnée liée" n'en portait AUCUN : jamais identifié comme "dépendant de la donnée", donc jamais régénéré, même si le nouveau HTML était déjà prêt côté serveur à chaque rendu. Correctif : nouveau marqueur "dataBound" (render_element_html.py), posé dès que _data_definition_id est réglé, reconnu par hasMarker(). Un exemplaire d'élément de jeu (ex. la boîte de dialogue posée sur une scène) porte ce marqueur EN PROFONDEUR dans son propre rendered_html dès qu'un de ses descendants internes en a un — il se retrouve donc bien régénéré dans son ensemble, sans changement supplémentaire nécessaire. Deux nouveaux tests, confirmés en échec sur l'ancien code (même scénario que le rapport : reproduit avec objet "dialog" + variable "dialog_order" + élément de jeu réutilisable) puis au vert avec le correctif. 155 tests au vert au total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
8cffbeac68 |
Donnée liée : conditions ET/OU illimitées + conditions de logique sur une variable globale
Deux fonctionnalités demandées, développées et corrigées dans cet
échange :
1. "Donnée liée" (Texte/Titre) : le réglage à 2 filtres fixes (toujours
combinés en ET) devient une liste de conditions ILLIMITÉE, avec un
choix ET/OU pour les combiner (screens/widgets/controls/c_clause_list.py,
screens/clause_list_codec.py). Rétrocompatible avec les anciens
éléments (_data_filtre_champ/_data_filtre2_champ), convertis à la
volée à la lecture, sans migration. Après un premier essai à la
présentation trop compacte et technique (retour utilisateur : "pas de
champ technique, pas de notation bizarre {{ }}"), la présentation
finale reprend EXACTEMENT l'ancien style (labels "Champ"/"...est"/
"...cette valeur", même sélecteur de valeur fixe/dynamique/variable
déjà existant, jamais la syntaxe brute), simplement répétée par
condition (templates/partials/clause_row.html), avec un bouton
"+ Ajouter une condition" bien visible et une liste scrollable
(static/style.css, .clauseListWrap). Le même moteur (filter_repeater_
rows.py généralisé) profite aussi au Répéteur de données en interne.
2. Nœud Condition de la Logique de la scène : peut désormais tester une
VARIABLE GLOBALE en plus d'un champ d'objet (cond_source/cond_variable/
cond_variable_chemin — screens/flow/ensure_flow_schema.py), sur la
clause principale ET chaque clause supplémentaire (ET/OU). Évalué côté
CLIENT (templates/play.html, evaluateConditionClause), contre un
nouveau gameData.variables exposé par full_game_payload.py — tenu à
jour par refreshRuntimeData() après toute action qui modifie une
variable, sans changement supplémentaire nécessaire. Le panneau de
condition reste utilisable même sans aucun objet défini dans le jeu
(avant, il disparaissait entièrement).
Vérifié : 153 tests pytest (nouveaux : test_data_binding_clause_list.py,
test_condition_variable.py) + logique JS d'évaluation des conditions
vérifiée isolément avec Node (variable scalaire, objet avec chemin
chaîné, tableau par index, variable introuvable, booléen, rétrocompatibilité
legacy) + rendu des deux pages (éditeur/jeu) vérifié sur le vrai projet
"test" en plus des jeux de test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |