8dc4b35dcfdd1006ca89af4dfc226fc3ad79045e
240
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e9d12945a9 |
Corrige la perte du compte admin à chaque déploiement : persiste /app/data
L'utilisateur signale devoir recréer un compte admin après chaque
déploiement en prod. Cause : docker-compose.prod.yml ne montait un
volume que pour /app/projects (les jeux) — la base des comptes créateurs
(data/users.db, voir auth/connection.py) et la clé de session Flask
(data/secret_key, voir core/flask_app.py::_load_or_create_secret_key)
vivaient toutes les deux dans /app/data, jamais monté : chaque nouveau
conteneur (à chaque déploiement) repartait d'un /app/data vide, donc
d'une base de comptes vide ("premier compte = admin" recommençait à
zéro) ET d'une nouvelle clé de session (tout le monde déconnecté, en
plus de la perte du compte).
Nouveau volume nommé forge_data:/app/data, à côté de forge_projects.
docker-entrypoint.sh corrige aussi sa propriété (root par défaut à la
création d'un volume nommé, comme pour forge_projects déjà) avant
d'abandonner les privilèges root.
Note : ce correctif ne prend effet qu'au déploiement SUIVANT sur main
(le conteneur actuellement en prod n'a pas ce volume) — un dernier compte
admin à recréer après ce déploiement, plus jamais ensuite.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
cc89b3f7e2 |
Corrige la CI (suite) : neutralise .dockerignore pour le build de test Python
"no tests ran ... file or directory not found: tests/" : .dockerignore (à la racine, pensé pour l'image de PROD buildée par build-and-push) exclut tests/ du contexte de build — COPY . . dans le Dockerfile jetable de test-python ne l'incluait donc jamais, quel que soit le Dockerfile utilisé (.dockerignore s'applique au contexte entier envoyé au démon, pas à un -f en particulier). Renomme .dockerignore avant ce build précis (le checkout de ce job est jetable, propre à lui, jamais repoussé vers le dépôt réel) — test-js n'a pas besoin du même correctif, il ne copie que static/js/play/, jamais exclu. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
d474303a55 |
Corrige la CI (suite) : exécute les tests pendant un docker build, pas dans un container de job
"container: image: python:3.13-slim" (tentative précédente) casse
actions/checkout@v4 : c'est une action Node.js, qui a besoin de Node
dans l'environnement d'exécution des steps — "container:" remplace CET
environnement en entier par l'image donnée, qui n'a pas Node
("command not found", nektos/act#107), pas seulement l'environnement
des commandes qu'on y lance soi-même.
Nouvelle approche : le job tourne sur le runner par défaut (checkout
fonctionne normalement, Node y est déjà disponible), et les tests
s'exécutent PENDANT un `docker build` (Dockerfile jetable passé par
stdin, jamais commité, un par langage) plutôt que dans un conteneur
lancé après coup — le transfert du contexte de build vers le démon
Docker passe par le protocole API (tar), jamais par un chemin hôte à
monter, donc insensible au problème Docker-outside-of-Docker qui avait
fait échouer le tout premier essai (docker run -v "$PWD":/app).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
8e8a159e88 |
Corrige la CI : les jobs de test tournent dans "container" au lieu d'un docker run imbriqué
Le job "test" échouait ("Could not open requirements file:
requirements-dev.txt") : docker run -v "$PWD":/app lancé DEPUIS un
runner qui exécute déjà le job dans son propre conteneur (Docker-
outside-of-Docker) ne peut pas monter "$PWD" — ce chemin vit dans le
conteneur du job, pas sur l'hôte où tourne le vrai démon Docker sollicité
par ce docker run imbriqué ; /app se retrouvait donc vide dans le
conteneur imbriqué.
Corrigé en utilisant la clé "container" (standard Gitea/GitHub Actions) :
le job tourne DIRECTEMENT dans l'image voulue, le checkout dépose les
fichiers dans son propre système de fichiers, aucun montage de volume à
faire. Un seul job "test" ne peut avoir qu'UNE image : scindé en
test-python (python:3.13-slim) et test-js (node:20-slim, pour les tests
node:test de static/js/play/__tests__/), build-and-push dépend des deux.
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>
|
||
|
|
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> |
||
|
|
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
|
||
|
|
c60884ec46 |
Corrige une régression fonctionnelle : revient à Bulma 1.0.2 (plus de Sass)
L'utilisateur a signalé des bugs réels en jeu après la refonte design system : jauges qui ne se remplissent plus, conditions de visibilité cassées sur une carte (enveloppe ouverte/fermée affichées en même temps). Racine du problème : Phase A avait basculé Bulma de 1.0.2 (CDN d'origine) vers 0.9.4, seule version compatible avec libsass (1.0.2 utilise le système de modules @use/@forward que libsass ne sait pas compiler). Or screens/rendering/render_jauge.py pilote le remplissage d'une jauge en fixant en ligne --bulma-progress-value-background-color — une variable CSS qui n'existe QUE dans le nouveau système de theming de Bulma 1.x, absente de 0.9.4. Aucune perte de données : les champs d'objet étaient toujours intacts en base (vérifié directement sur projects/test/game.db) — uniquement un problème de rendu/comportement en jeu. Correction : revient à Bulma 1.0.2, vendoré tel quel et non modifié (static/vendor/bulma.min.css, ~677 Ko, auto-hébergé — toujours aucune dépendance CDN). Bulma 1.x expose déjà tout son thème via de vraies variables CSS (--bulma-primary-h/-s/-l, --bulma-radius...), justement conçues pour être surchargées après coup SANS recompilation Sass — styles/bulma-override.css les redéfinit avec la palette Forge (teintes HSL calculées à partir des couleurs de la charte). build_css.py devient un simple concaténage de 4 fichiers (bulma.min.css + bulma-override.css + forge-tokens.css + forge-custom.css), plus besoin de libsass ni d'aucun compilateur — supprimé de requirements.txt. styles/bulma/ (source Sass 0.9.4 vendorée en Phase A) et styles/forge-theme.scss supprimés. Nouvelle règle ajoutée en commentaire dans bulma-override.css : ne plus jamais changer de version de Bulma sans `grep -rn "\-\-bulma-" screens/ templates/` d'abord — cette dépendance n'est pas que visuelle. 197 tests toujours verts (ils ne couvrent que le HTML généré, jamais le rendu réel — c'est pour ça que cette régression n'avait pas été détectée avant que l'utilisateur ne la signale en jouant pour de vrai). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
3032b27740 |
Corrige le QR code de la 2FA invisible à l'inscription
Deux bugs cumulés dans auth/totp_qrcode_svg.py :
- SvgImage (variante utilisée) ne pose aucun attribut viewBox sur le
<svg> racine — la règle CSS qui le fait tenir dans son cadre
(.totpQrWrap svg { width:100% }) n'avait donc rien à quoi se raccorder
pour mettre à l'échelle le dessin interne (coordonnées en mm) : le QR
code restait invisible/coupé. Remplacé par SvgPathImage, seule variante
pure Python de qrcode dont le <svg> racine inclut un viewBox.
- qrcode.make() préfixe toujours sa sortie d'une déclaration XML
("<?xml version=...?>"), valide pour un fichier .svg autonome mais
invalide au milieu d'un document HTML — ne garder que ce qui commence
à "<svg" avant de l'insérer dans la page.
165 tests toujours au vert. Le compte non confirmé resté bloqué sur cet
écran (vandal.william@forgebase.fr) a été supprimé de data/users.db : la
prochaine inscription redevient bien le tout premier compte (admin).
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>
|
||
|
|
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 (
|
||
|
|
d87d6addce |
Boîte de dialogue (superposition) stylisée avec les classes Bulma
Le widget "Superposition / boîte de dialogue" gagne les classes Bulma "modal is-active" (sur le voile plein écran) et "box" (sur la boîte centrée), en PLUS du style inline existant — jamais à sa place : tout le positionnement/masquage critique (position:fixed, z-index, display) reste en inline, qui gagne toujours sur une règle de classe. Si Bulma (chargé depuis un CDN, voir play.html) ne se charge pas (hors-ligne), la boîte de dialogue continue de fonctionner exactement pareil — ces classes n'ajoutent qu'un habillage visuel (ombre, base de police/espacement Bulma) qui se dégrade sans rien casser. Pas de ".modal-background" séparé (convention Bulma habituelle) : le voile semi-transparent est déjà posé en inline sur la même balise (background:rgba(...)), un second calque tout aussi transparent par-dessus n'aurait ajouté qu'un assombrissement redondant. 137 tests toujours au vert (changement purement visuel, aucune assertion cassée) ; jeu de démo régénéré et vérifié (classes bien présentes dans le rendu). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
7dd7551bc5 |
Corrige le texte illisible dans les boîtes de dialogue du jeu de démo
Pas un bug moteur cette fois : le moteur ne fige JAMAIS de couleur de texte par défaut à la création d'un élément (voir default_style_for_widget.py) — un titre/texte fraîchement posé retombe donc sur le texte SOMBRE par défaut de Bulma, pensé pour un fond clair. Mon script de démo ne posait jamais explicitement de couleur de texte sur les titres/paragraphes à l'intérieur des boîtes de dialogue (fond sombre volontaire) : le texte y était donc quasi invisible, ce qui donnait l'impression trompeuse que "la boîte de dialogue est assombrie" — alors que seul son texte, invisible par défaut sur fond sombre, l'était (la boîte elle-même a bien sa couleur opaque demandée, #20263a/#1f3a24/ #2a2440, sans aucun voile supplémentaire dessus). Ajout de la couleur de texte manquante (#f5f6fa) sur chaque titre/texte posé dans une boîte de dialogue. Valeur volontairement différente de la suggestion par défaut du panneau (#e8eaf0) : save_element_controls.py ignore délibérément un réglage renvoyé identique à sa valeur par défaut tant qu'il n'a jamais été personnalisé (même logique anti-figeage que default_style_for_widget.py) — envoyer #e8eaf0 tel quel n'aurait donc eu AUCUN effet, silencieusement (piège rencontré et documenté dans le script). Jeu de démo régénéré (supprimé puis reconstruit) avec le correctif. 129 tests toujours au vert (script indépendant, aucun changement moteur). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
28d8cd8cd0 |
Ajoute un script de démo pour tester dialogues/surbrillance/message différé
Les trois mécanismes de guidage du joueur discutés (boîte de dialogue en
popup, réaction à un clic, mise en évidence d'un élément à cliquer)
existent DÉJÀ dans le moteur (voir README, section "Survol, séquences
temporisées, surbrillance, overlay, verrouillage") : widget "Superposition
/ boîte de dialogue", action "Modifier un élément → Surbrillance", et
déclencheur "affichage" combiné à l'action "Attendre" pour un message qui
arrive tout seul après quelques secondes. Rien à coder côté moteur.
scripts/build_demo_dialogues.py construit, via les VRAIES routes Flask
(mêmes routes qu'utilise le navigateur), un jeu de démo persistant
("Demo Dialogues Surbrillance") avec deux écrans :
- "Tutoriel" : dialogue de bienvenue à l'ouverture de l'écran -> son
bouton OK ferme le dialogue et met un autre bouton en surbrillance ->
cliquer ce bouton l'éteint, le désactive (verrouillage anti-reclic) et
ouvre un dialogue "Bravo" en réaction, fermable à son tour.
- "Message différé" : un dialogue "mentor" apparaît tout seul 3 secondes
après l'affichage de l'écran (affichage -> attendre -> visibilité),
sans aucune action du joueur.
Vérifié via /runtime-payload (nombre de nœuds/arêtes attendu sur les deux
écrans) et /play (overlay + surbrillance + attente bien exposés côté
rendu). 128 tests toujours au vert (script indépendant, aucun changement
moteur).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|