bab581737ab02a016407326ca1e92945b60ef669
111
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
92fbfbc9dd |
Phase 4 : ajout dynamique d'une ligne à l'exécution
Nouveau sentinel LAST_INSERTED_ROW_ID = -3 (screens/flow/constants.py), aux côtés de CLICKED_ROW_ID = -1 — même principe : un id de ligne n'existe qu'APRÈS l'insertion, jamais connu à la création du nœud. Permet d'enchaîner un nœud "Ajouter une ligne" (crée une ligne VIDE) puis un ou plusieurs nœuds "Modifier une donnée" déjà existants (ciblant "➕ Dernière ligne ajoutée" dans le sélecteur "Ligne concernée", partagé avec les conditions) pour renseigner ses champs — réutilise 100% du mécanisme actuel, aucun nouveau format de payload multi-champs. screens/data_actions/apply_add_row_action.py : db.get_definition + db.insert_row(slug, definition, {}, player_id) — la ligne appartient au joueur qui agit pour un objet per_player (Phase 1). Nouvelles routes (créateur ET publique, comme prévu dès la Phase 1) : POST /game/<slug>/flow/nodes/<id>/run-add-row et son miroir /jouer/<slug>/.../run-add-row — renvoient {"ok", "row_id"}. routes/flow/flow_node_run_data.py (+ son miroir public) : résout aussi LAST_INSERTED_ROW_ID (en plus de CLICKED_ROW_ID déjà en place) via last_inserted_row_id transmis par le client. static/js/play/actions.js : runActionNode branche "ajouter_ligne" -> fetch la nouvelle route, pose window.lastInsertedRowId, puis refreshRuntimeData() (un Répéteur lié affiche la nouvelle ligne au prochain rendu, confirmé par l'audit préalable — aucun ajustement du mécanisme de rafraîchissement nécessaire). La branche "modifier_donnee" transmet désormais aussi last_inserted_row_id, comme clicked_row_id. templates/screen_edit.html + static/js/screen_edit/flow-editor.js : nouveau type d'action "Ajouter une ligne à un objet" (juste un sélecteur d'objet, aucun champ à remplir — le rappel du fonctionnement enchaîné est affiché directement dans le formulaire) ; le sélecteur "Ligne concernée" (partagé Condition/Modifier une donnée) gagne l'option "➕ Dernière ligne ajoutée" à côté de "🖱️ Ligne cliquée". Vérifié : 259 tests passent (4 nouveaux, dont un bout-en-bout via HTTP qui enchaîne réellement les deux nœuds et vérifie le champ renseigné, et un qui verrouille l'isolation par joueur de la ligne créée), 13 tests node:test toujours au vert, syntaxe JS validée. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
727c3c97ba |
Phase 3 : déclencheur clavier + minuteur récurrent
screens/flow/constants.py : TRIGGER_EVENTS += "clavier" (À l'appui sur une touche) et "minuteur" (Toutes les X millisecondes) — ni élément ni écran précis pour les deux, comme "evenement" déjà en place. FLOW_NODE_FIELDS += trigger_key/trigger_interval_ms. ensure_flow_schema.py : ALTER TABLE pour les 2 colonnes (patron trigger_custom_event_id). templates/screen_edit.html + static/js/screen_edit/flow-editor.js : - "clavier" : un champ "Touche à surveiller" qui capture lui-même la touche pressée (onkeydown sur l'input, captureFlowTriggerKey()) plutôt que de faire deviner la syntaxe attendue (ev.key du navigateur, ex. "ArrowUp", "a", " " pour Espace). - "minuteur" : un simple champ numérique (millisecondes). - nodeLabel() affiche "⌨️ Touche « X »"/"⏱️ Toutes les N ms" sur le nœud. static/js/play/triggers.js (moteur de jeu) : - bindKeyboardTriggers() : UN SEUL window.addEventListener('keydown', ...) posé une fois pour tout le jeu (voir l'amorçage en fin de templates/play.html) — même patron de scan global que dispatchGameEvent() pour "Sur un événement personnalisé". - runScreenTimerTriggers(screenId) : géré PAR ÉCRAN (appelé depuis showScreen(), static/js/play/screens.js) — démarre les setInterval des nœuds "minuteur" de l'écran affiché, arrête d'abord tous ceux de l'affichage précédent (même principe que runAnimationTimeline) pour ne jamais accumuler des minuteurs sur des écrans quittés. Vérifié : 255 tests passent (5 nouveaux, dont un qui verrouille que la touche Espace — très probablement utilisée en jeu — n'est pas filtrée comme une valeur "vide" par add_flow_node.py), 13 tests node:test toujours au vert, syntaxe JS validée sur tous les fichiers de static/js/play/ et static/js/screen_edit/. Comme le reste du graphe de logique côté client, le comportement RÉEL d'un keydown/setInterval n'est pas testable sans navigateur — test manuel recommandé (touche assignée à un saut, minuteur faisant avancer un compteur). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
7476ed229e |
Phase 2 : hasard + opérations mathématiques
screens/labels/data_operations.py : 6 nouvelles opérations pour "Modifier une donnée"/"Modifier une variable" — multiplier, diviser, modulo (garde-fou division par zéro : valeur inchangée plutôt qu'une ZeroDivisionError qui interromprait le graphe), minimum/maximum (borne la valeur ACTUELLE — utile pour une variable globale, qui n'a pas de min_value/max_value comme un champ d'objet), et alea (tire un nombre aléatoire entre deux bornes). screens/data_actions/compute_operation.py (déjà factorisé en Phase 0, donc une seule implémentation pour apply_data_action.py/ apply_variable_action.py) : implémente les 6. "alea" est la seule à deux opérandes — réutilise data_value au format "min,max" plutôt qu'une nouvelle colonne de nœud (bornes remises dans l'ordre si inversées). random.uniform pour un résultat décimal, random.randint pour un entier. static/js/screen_edit/flow-editor.js + templates/screen_edit.html : petit indice visuel — le champ "Valeur / montant" du formulaire de nœud affiche "min,max (ex. 1,6)" quand "alea" est choisi, pour ne pas laisser deviner ce format à deux nombres, différent de toutes les autres opérations. Sinon aucun nouveau champ/changement de schéma nécessaire, le <select> était déjà généré depuis DATA_OPERATIONS. Vérifié : 250 tests passent (19 dans test_compute_operation.py, dont un qui a dû être corrigé — il utilisait "multiplier" comme exemple d'opération INCONNUE, devenu un mauvais exemple maintenant qu'elle existe), 13 tests node:test toujours au vert, syntaxe JS validée. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
236d6b4b46 |
Phase 1 (1/3) : état par joueur — couche db/ et chaîne de rendu
Fil directeur du plan : chaque variable globale et chaque objet de données gagne un player_id (sentinelle PLAYER_SHARED='__shared__' par défaut partout — l'aperçu créateur et tous les tests existants continuent de fonctionner À L'IDENTIQUE, aucune signature appelée sans argument explicite ne change de comportement), plus un réglage per_player choisi une fois à la création : - per_player=1 (par défaut) : chaque joueur a sa propre valeur/ses propres lignes. - per_player=0 : valeur/lignes partagées par tous les joueurs (ex. un compteur de visiteurs global, un catalogue commun). db/global_vars/ : _global_variables passe de UNIQUE(name) à UNIQUE(name, player_id) — SQLite ne permet pas de modifier une contrainte UNIQUE via ALTER TABLE, reconstruction de la table détectée et faite une seule fois (ensure_global_vars_schema.py) pour les jeux créés avant cette phase. La ligne "modèle" (player_id=PLAYER_SHARED, créée par le créateur) porte le réglage per_player et sert de valeur PAR DÉFAUT : la première écriture d'un joueur sur une variable per_player crée paresseusement SA propre ligne (copiée depuis le modèle) ; une lecture sans ligne encore écrite retombe sur le modèle (nouveau resolve_player_key.py). list_global_variables() (tableau de bord) ne montre toujours que les lignes modèles ; nouveau list_global_variables_for_player() expose la valeur EFFECTIVE d'un joueur au runtime (full_game_payload.py). db/definitions/ + db/rows/ : chaque table d'objet généré (create_definition.py) gagne une colonne player_id (ADD COLUMN simple, pas de contrainte UNIQUE en jeu ici) ; _definitions gagne per_player. Contrairement aux variables, PAS de repli sur une ligne "modèle" pour les lignes d'un objet per_player — une LISTE n'a pas de valeur par défaut unique à copier comme un scalaire, un nouvel objet per_player démarre VIDE pour chaque joueur (nouveau resolve_row_player_key.py). get_row/update_row/update_row_field/delete_row filtrent aussi par player_id (pas seulement id) : garde-fou contre un row_id d'un AUTRE joueur, nécessaire dès qu'un objet per_player sera exposé sur la future route publique /jouer/<slug>. Migration : nouveau ensure_player_id_column(slug, table_name), appelé avant toute requête sur une table d'objet créée avant cette phase. Chaîne de rendu (screens/elements/list_elements.py -> screens/rendering/render_element_html.py -> render_repeater.py/ render_jauge.py/resolve_bound_row.py/visibility_condition.py/ filter_repeater_rows.py) : player_id transite dans le ctx déjà utilisé partout pour "champ en cours" (ctx["_forge_player_id"], même patron que ctx["_forge_play_mode"], posé une seule fois par list_elements quand enforce_visibility=True) — pas de nouveau paramètre positionnel à threader dans chaque fonction, juste une clé de plus dans un mécanisme déjà en place. Nouveau tests/test_player_state.py : verrouille à la fois le nouveau comportement (deux joueurs => valeurs/lignes indépendantes ; per_player=0 => partagé ; nouveau joueur => valeur par défaut pour une variable, liste VIDE pour un objet ; get_row ne fuite jamais vers un autre joueur) et la non-régression de l'aperçu créateur (comportement historique inchangé). Vérifié : 232 tests passent (9 nouveaux). Reste à faire (prochains commits) : route publique /jouer/<slug>, identité visiteur (cookie), bascule "Publier en ligne" dans le tableau de bord. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
bb84b7b377 |
Phase 0 : factorise compute_new_value + ajoute pytest/node test à la CI
1. Duplication éliminée avant que la Phase 2 (hasard/opérations mathématiques) n'en ajoute 6 de plus aux DEUX fichiers : la chaîne d'opérations quasi identique entre screens/data_actions/ apply_data_action.py (champ d'objet) et apply_variable_action.py (variable globale) est factorisée dans un nouveau compute_operation.py::compute_new_value(operation, current, raw_value, is_decimal), réutilisé par les deux. Nouveau tests/test_compute_operation.py verrouille le comportement des 7 opérations existantes (dont les cas limites : valeur invalide, type décimal vs entier, opération inconnue) avant d'en ajouter d'autres. 2. .gitea/workflows/deploy.yml déployait en prod à chaque push sur main sans jamais exécuter la suite de tests — rien ne bloquait techniquement un commit cassé. Nouveau job "test" (pytest + node:test sur la logique pure de static/js/play/, via des conteneurs officiels plutôt que des actions du marketplace, cohérent avec le choix déjà fait dans ce fichier) tourne sur CHAQUE push (main ET dev, utile pour ce dépôt qui travaille sur dev) ; "build-and-push"/"deploy" gagnent un "needs: test" et restent réservés à main (filtre sur gitea.ref) — un push sur dev ne redéploie jamais la prod, seulement les tests. Vérifié : 223 tests passent (8 nouveaux), YAML validé. 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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
dcbec16818 |
Ajoute les événements personnalisés (backend) : déclencher/écouter
Première moitié de la fonctionnalité "événements" (NEED_ACTION et autres) : une entité game-wide (nom, description, "a un paramètre élément" oui/non), déclenchable comme nouvelle action du graphe de logique depuis n'importe quelle scène/modèle, et écoutable comme nouveau type de déclencheur depuis n'importe quel autre. L'UI (nouvel onglet "Événements" dans screen_edit.html, formulaires de nœud, exécution côté client dans play.html) suit dans un commit séparé. - db/custom_events/ (calqué sur db/global_vars/) : CRUD de la table _custom_events (nom unique, description, has_element_param). create_custom_event est idempotent par nom (même convention que create_global_variable) — sans risque en cas de double soumission. - screens/flow/ : 3 nouvelles colonnes sur _flow_nodes (trigger_custom_event_id/target_custom_event_id : quel événement un nœud écoute/déclenche ; target_element_from_event : indicateur réutilisable par n'importe quel nœud Action utilisant déjà target_element_id, pour résoudre "l'élément transmis par l'événement en cours" au lieu d'une cible fixe — contourne la contrainte de clé étrangère de target_element_id, qui empêche d'y stocker un sentinel comme EVENT_ROW_ID directement). Nouveau trigger_event "evenement" et action_type "declencher_evenement". - screens/custom_events/ (PAS dans db/, même séparation que screens/elements/delete_element.py) : delete_custom_event, la SEULE suppression d'entité game-wide du moteur à vraiment cascader (demande explicite) — supprime tous les nœuds/arêtes qui référencent l'événement, sur TOUTES les scènes ET tous les modèles à la fois (aucun filtre screen_id nécessaire : un modèle est un écran caché, même table _flow_nodes). list_custom_event_usages : où un événement est écouté/déclenché, pour l'onglet Événements à venir. - routes/custom_events/ : CRUD monté sous /game/<slug>/events/..., redirige vers l'éditeur de scène/modèle d'origine (screen_id transmis par le formulaire) avec l'onglet "events" à ouvrir. tests/test_custom_events.py (nouveau) : idempotence à la création, usages détectés sur deux écrans différents, suppression qui retire bien les DEUX nœuds (un sur une vraie scène, un sur un modèle/écran caché) en une seule opération, sans toucher aux écrans eux-mêmes. 207 tests au total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
989e899a96 |
Ajoute un anti-bruteforce (3 essais libres puis 5/10/20/40 min, plafonné à 1h)
Sur la connexion (mot de passe + code 2FA, même compteur pour les deux — un attaquant qui connaît le mot de passe ne doit pas avoir un nombre illimité d'essais sur le code) et sur la confirmation 2FA de l'inscription : 3 tentatives libres, puis un verrouillage qui double à chaque nouvel échec (5, 10, 20, 40 minutes...), plafonné à 1h (auth/rate_limit.py). Remis à zéro dès une connexion RÉELLEMENT aboutie (mot de passe ET code corrects) — jamais sur le seul succès du mot de passe, pour ne jamais donner un nombre illimité d'essais sur le 2FA à qui connaît déjà le mot de passe. Le verrouillage est annoncé IMMÉDIATEMENT sur la réponse qui le déclenche (record_failed_attempt renvoie la durée qu'il vient de poser), pas seulement découvert au prochain essai. Colonnes ajoutées en ALTER TABLE (failed_attempts, locked_until) pour ne rien casser sur une base de comptes déjà créée avant cette fonctionnalité. 14 tests dans test_auth.py (dont l'escalade 5/10/20/40/60, le blocage même avec le bon mot de passe une fois verrouillé, et la remise à zéro sur connexion réussie). 169 tests au total, tous au vert. 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> |
||
|
|
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>
|
||
|
|
7c237d6f1c |
Retire le voile plein écran et le forçage de visibilité de l'éditeur (retour utilisateur)
Deux retours après le dernier correctif (
|
||
|
|
6e46949cf8 |
Corrige la boîte de dialogue posée comme élément de jeu réutilisable : conteneur vide au lieu du dialogue
Bug rapporté : poser un élément de jeu ("Mes éléments de jeu") dont le
modèle n'est qu'une "Superposition / boîte de dialogue" sur une vraie
scène affichait un conteneur vide à l'endroit du dépôt — jamais le
dialogue. La boîte de dialogue existait bien (masquée comme prévu, en
attente d'une action qui l'affiche) : c'est l'enveloppe qui l'entoure qui
n'aurait jamais dû être visible.
Cause : tout exemplaire d'élément de jeu est posé avec le widget générique
"conteneur" par défaut (add_element.py, colonne default_widget) — son
contenu réel (le modèle) est rechargé EN DIRECT à l'intérieur
(_render_element_type_children), mais l'enveloppe "conteneur" elle-même
reste une boîte NORMALE, toujours visible, avec sa propre couleur de fond/
bordure et sa position fixe sur le canevas (contrairement à une
superposition posée directement, qui, elle, démarre masquée). Résultat :
une boîte vide et permanente à l'endroit du dépôt, pendant que le vrai
dialogue (démarré masqué, correctement) reste invisible en dessous/
au-dessus tant qu'aucune action ne le déclenche.
Correctif : quand le modèle ENTIER d'un élément de jeu n'est qu'une seule
superposition (screens/element_types/is_overlay_only.py, nouveau), on
court-circuite entièrement l'enveloppe "conteneur" (render_element_html.py)
et on retire aussi le z-index de son cadre de positionnement
(element_style_filter.py, list_elements.py) — même raison que pour une
superposition posée directement (
|
||
|
|
78a373a536 |
Corrige la vraie cause : la boîte de dialogue restait invisible DANS L'ÉDITEUR (masquée par défaut)
Après vérification serveur, le texte était bel et bien rendu dans le HTML (couleur correcte incluse) — donc pas un souci de contenu ni de couleur. La vraie cause : le widget "Superposition / boîte de dialogue" démarre MASQUÉ par défaut à sa création (display:none, voir default_style_for_widget.py — pour ne pas couvrir tout l'écran dès qu'on le pose). Ce display:none est écrit tel quel dans le HTML aussi bien en mode jouable QUE dans l'éditeur, puisque le rendu de ce widget est partagé par les deux. Résultat : toute la boîte (et donc son contenu, peu importe le texte ou sa couleur) restait invisible dans le CANEVAS DE L'ÉDITEUR dès l'instant de sa création — impossible d'y voir/positionner visuellement ce qu'on pose dedans tant qu'on n'a pas pensé à basculer manuellement "Visibilité" sur "Visible" (puis à y repenser pour la remettre sur "Masqué" avant de tester en jeu). Correctif (render_overlay.py) : dans l'ÉDITEUR uniquement (détecté via le ctx "_forge_play_mode" déjà posé par list_elements.py pour le mode jouable), un "display:flex" est ajouté en dernier dans le style — gagnant sur le "display:none" par défaut (CSS : la dernière déclaration de la même propriété l'emporte). Le mode JOUABLE, lui, continue de respecter ce réglage normalement (masqué tant qu'aucune action ne l'affiche). Nouveau test, confirmé en échec sur l'ancien code puis au vert avec le correctif : vérifie explicitement que le style se termine par "display:flex;" dans l'éditeur et par "display:none;" en mode jouable. 139 tests au vert au total. Jeu de démo régénéré. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
69bafcb5c5 |
Corrige le texte invisible dans la boîte de dialogue (régression du style Bulma)
Régression du commit précédent (
|
||
|
|
7c0f1ed427 |
Corrige la régression du correctif overlay : /elements/<id>/geometry plantait (500)
Le correctif précédent ( |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|