Le correctif précédent (81c31a9) retirait TOUTE position/taille du cadre
.canvasElement/.playElement d'une "superposition" pour ne plus piéger son
propre z-index:9999 dans un contexte d'empilement local — mais ce même
cadre sert aussi, dans l'ÉDITEUR, de prise pour le glisser-déposer/
redimensionnement de ce widget (screen_edit.html). Sans
position:absolute + left/top/width/height, ce cadre s'effondre à 0×0 (son
contenu réel est en position:fixed, hors flux) : le calcul de geometrie
divise alors par une dimension nulle, produit NaN, et
JSON.stringify(NaN) envoie littéralement "null" au serveur — qui plantait
sur float(None) dans routes/elements/element_geometry.py (500, reproduit
par l'utilisateur en posant un template lié à un objet et en essayant de
repositionner l'overlay dans l'éditeur).
Correctif plus ciblé : le cadre garde position/left/top/width/height comme
n'importe quel widget (l'éditeur redevient fonctionnel) — SEUL le z-index
est omis pour ce widget précis. position:absolute SANS z-index explicite
(donc z-index:auto) ne crée PAS de nouveau contexte d'empilement CSS :
le z-index:9999 posé plus profond (render_overlay.py) continue donc de se
comparer directement aux autres éléments de l'écran, et gagne toujours —
le bug de superposition visuelle (81c31a9) reste corrigé, sans regression
sur l'éditeur cette fois.
test_confort.py mis à jour (position:absolute attendu, plus d'assertion
"position:static") + nouveau test qui appelle directement la route
/geometry sur une superposition pour confirmer qu'elle répond 200.
137 tests au vert au total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Régression du commit précédent (d87d6ad, classes Bulma sur la
superposition) : la classe ".box" impose elle-même une couleur de texte
SOMBRE (pensée pour un fond blanc). Comme aucun widget ne fige de couleur
de texte à sa création (default_style_for_widget.py), un titre/texte posé
dans la boîte SANS couleur personnalisée héritait de ce gris sombre
imposé par Bulma — invisible sur le fond sombre par défaut de cette boîte
de dialogue. Ni erreur serveur ni régression de test visible : juste un
texte "présent mais invisible", exactement ce qui a été rapporté ("j'ai
mis un texte dedans mais il ne se voit pas").
Correctif : la boîte fixe elle-même une couleur de texte claire par
défaut (color:#e8eaf0) dans son propre style inline — l'élément le plus
proche gagne, donc ça écrase la couleur imposée par la classe Bulma, tout
en restant surchargeable : un titre/texte qui personnalise sa propre
couleur (réglage "Couleur du texte") continue de l'emporter normalement.
Nouveau test de régression, confirmé en échec sur le commit précédent
(même style Bulma, pas encore cette couleur) puis au vert avec le
correctif. 138 tests au vert au total. Jeu de démo régénéré.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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 (81c31a9) : sans ça, ce cadre reste un
candidat à piéger le z-index:9999 du dialogue rendu à l'intérieur dès
qu'un autre élément de la scène a un z-index plus grand.
Nouveau test, confirmé en échec sur l'ancien code (même "class=\"box\""
fantôme reproduit) puis au vert avec le correctif. 140 tests au vert au
total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deux retours après le dernier correctif (78a373a, qui forçait
"display:flex" dans l'éditeur pour que la boîte de dialogue reste
visible) :
- "je souhaite avoir la main sur la visibilité de la modale sinon elle
s'affiche toujours sur la scène, court-circuite ma logique" — le
forçage empêchait de vraiment utiliser "Visibilité" pendant l'édition.
- "quand j'édite la modale ou quand je la mets dans une scène je
souhaite que rien ne soit assombri, l'assombrissement ne se fait que
quand la scène est jouée" — le voile plein écran (position:fixed +
fond assombri) restait aussi actif dans l'éditeur.
Correctif (render_overlay.py) : le voile plein écran ET le forçage de
visibilité sont retirés de l'ÉDITEUR — ce widget s'y comporte maintenant
comme un CONTENEUR NORMAL (position/taille selon x/y/width/height, aucun
voile, réglage "Visibilité" respecté normalement, comme n'importe quel
autre widget masqué). Le comportement plein écran/voile/masquage par
défaut n'est conservé qu'en mode JOUABLE (ctx["_forge_play_mode"]).
En creusant pourquoi "Visible" ne suffisait pas à faire réapparaître la
boîte dans l'éditeur (menant l'utilisateur à essayer "Invisible" à la
place, visible dans ses captures), trouvé un vrai bug latent dans
save_element_controls.py : l'option "Visible" du réglage "Visibilité" ne
touche volontairement jamais "display" (pour ne pas écraser le
"display:flex" d'un conteneur en disposition ligne/colonne — voir
visibility_control.py) — ça fonctionne seulement parce que, pour un
widget AVEC un réglage "Disposition interne", celui-ci réaffirme lui-même
un display non-"none" au même enregistrement. La "superposition" n'a PAS
ce réglage : "display:none" (posé à la création ou par un "Masqué"
précédent) restait donc bloqué pour toujours, quel que soit le nombre de
fois où "Visible" était ensuite choisi. Corrigé : "Visible" efface aussi
"display" pour tout widget SANS réglage "Disposition interne" (safe : les
widgets qui EN ont un ne sont pas concernés, donc aucune régression sur
leur comportement existant).
Tests mis à jour (l'ancien test attendait le forçage, désormais retiré) +
nouveau test qui couvre le cycle complet (masqué par défaut -> "Visible"
choisi -> apparaît sans voile dans l'éditeur -> voile plein écran
retrouvé en mode jouable). 140 tests au vert au total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
Suite du correctif précédent (124f250, marqueur "dataBound") : le
rafraîchissement fonctionnait, mais remplaçait tout l'élément de PREMIER
NIVEAU marqué — pour un Texte "Donnée liée" posé DANS un élément de jeu
réutilisable (ex. une boîte de dialogue), le seul élément de premier
niveau existant est l'EXEMPLAIRE lui-même (ses descendants internes ne
sont jamais des entrées séparées de la scène, voir list_elements.py) :
tout l'exemplaire — voile plein écran, boîte, tout — était donc remplacé
d'un bloc, ce qui le faisait visuellement disparaître puis réapparaître
pour un simple changement de texte à l'intérieur.
Correctif (templates/play.html, refreshRuntimeData()) : avant de
remplacer un élément marqué en bloc, on cherche d'abord, DANS le nouveau
fragment, des descendants plus précis portant eux-mêmes un marqueur en
commentaire ("visibilityGated"/"dataBound" — jamais "repeaterItem"/
"jaugeBar", qui restent volontairement régénérés en bloc, un
comportement déjà correct pour un Répéteur/une Jauge). S'il en existe,
seuls CES éléments précis sont patchés individuellement (retrouvés via
leur propre data-element-id) ; sinon, comportement inchangé (remplace
l'élément entier, cas normal d'un Texte "Donnée liée" posé directement
sur une scène, hors élément de jeu réutilisable).
155 tests toujours au vert (changement purement côté client — le
rendu serveur et la présence du marqueur, eux, étaient déjà couverts par
les tests du commit précédent). Vérifié structurellement (adjacence
</p><!--dataBound--> confirmée dans le rendu réel du projet "test").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 (81c31a9) : sans ça, ce cadre reste un candidat à piéger le z-index:9999 du dialogue rendu à l'intérieur dès qu'un autre élément de la scène a un z-index plus grand. Nouveau test, confirmé en échec sur l'ancien code (même "class=\"box\"" fantôme reproduit) puis au vert avec le correctif. 140 tests au vert au total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>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>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>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>