Skip to content

Dev - #126

Merged
magicgg91 merged 8 commits into
mainfrom
dev
Sep 15, 2026
Merged

Dev#126
magicgg91 merged 8 commits into
mainfrom
dev

Conversation

@magicgg91

Copy link
Copy Markdown
Owner

No description provided.

claude and others added 8 commits September 14, 2026 19:38
Numéroté 98 : le 97 est pris par le correctif CI mergé juste avant.

A. Les valeurs d'Argent saisies et enregistrées en admin n'étaient jamais
   regardées par le référentiel public. Trois choses les ignoraient, et
   toutes nommaient une ligue en dur :

   - `levelUpTroopsAt` s'ouvrait sur `if (league === "silver") return null`
   - `confirmedLevelUpLeagues`, une liste statique de 5 noms
   - le message public listait ces 5 noms dans son texte traduit

   La disponibilité vient maintenant des données : `hasLevelUpTroopsFormula`
   répond oui quand coefficient ET ratio sont présents et > 0, pour
   n'importe quelle ligue. Une ligue renseignée s'affiche, une ligue vidée
   redevient non confirmée, sans aucun nom de ligue dans la logique — un
   test le vérifie sur le source lui-même (hors commentaires).

   Le message nomme désormais les ligues réellement disponibles, assemblées
   par `Intl.ListFormat` pour que le « A, B et C » reste correct dans les
   5 langues. Formulation retouchée en conséquence dans les 5 fichiers de
   messages, avec une branche ICU pour le cas où aucune ligue n'en a.

A-bis (trouvé en route, non signalé au brief). La route PUT refusait tout
   payload contenant un zéro, donc le référentiel Progression était
   insauvegardable tant qu'une ligue restait vierge — sur une installation
   neuve, c'est Argent, et la toute première sauvegarde revenait en 400.
   Or c'est précisément {0, 0} qui marque une ligue non confirmée. La règle
   devient : une paire est soit renseignée (les deux > 0), soit vierge (les
   deux à 0) ; à moitié remplie, négative ou NaN, elle reste refusée.

B. Le « (Formule de troupes non confirmée) » de l'éditeur admin suit
   maintenant ce qui est réellement stocké : il disparaît dès que la ligue
   a ses deux valeurs, et apparaît sur n'importe quelle autre ligue laissée
   vide — il n'était pas propre à Argent.

C. Les ligues sont listées dans l'ordre de progression du jeu (Bronze →
   Légende, l'ordre de la constante `leagues`), côté admin comme public.
   Le public l'était déjà ; l'admin affichait Argent en dernier, après
   Légende, parce que sa ligne était écrite à la main à la suite des cinq
   « confirmées ».

Tests : 21 unitaires (prédicat, validation de sauvegarde, ordre, absence
de nom de ligue dans le source, note admin conditionnelle) et 1 e2e qui
refait le parcours signalé — constat de l'indisponibilité, saisie côté
admin, puis table d'Argent affichée en public avec la valeur calculée
depuis les données enregistrées, et retour à l'état vierge.
Contre-vérifié en cassant le correctif : le littéral silver remis en
place rougit 3 unitaires et l'e2e ; la note admin rendue inconditionnelle
en rougit 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJgsDSfsCbbn8cochFGwe2
…gues

Bloc 98 — 🐛 Progression : une ligue disponible parce qu'elle a ses valeurs, pas parce qu'elle est dans une liste
Bug utilisateur : un transfert depuis le simulateur Stuff vers les
paramètres du joueur pouvait revenir à zéro tout seul, peu après le
chargement de la page. C'est aussi ce qui rendait
player-settings-panel.test.tsx instable en CI depuis le Bloc 93.

Cause. `safePlayerSettings` étalait l'objet stocké en entier, donc `v`
— de la comptabilité de stockage, absente de `PlayerSettings` — repartait
à l'intérieur des réglages. Toute comparaison entre la valeur en mémoire
et la valeur relue était donc inégale par construction, quel que soit le
contenu. La garde de `syncFromStorage` en est une : le panneau répondait
à sa propre sauvegarde par un nouvel objet, et le cycle
écriture/diffusion supplémentaire qui suivait portait un instantané
d'avant le transfert — instantané qui écrasait l'écriture externe arrivée
entre-temps.

Correctif : `v` est séparé des réglages à la lecture (déstructuration),
et sert uniquement à décider de la migration v1 → v2 comme avant. Rien
d'autre ne change : la sérialisation continue d'estampiller.

Mesure, comparaison contrôlée (même charge CPU, 6 boucles saturantes,
60 exécutions du fichier de test dans chaque bras) :

  avant : 6 échecs / 60
  après : 0 échec  / 60

0/60 ne prouve pas l'impossibilité, mais la cause est corrigée de façon
déterministe et deux tests l'épinglent : `safePlayerSettings` rend les
réglages seuls (mêmes clés que `defaultPlayerSettings`, comparaison JSON
égale), et un montage ne produit plus qu'une seule sauvegarde/diffusion
au lieu de deux. Les deux rougissent sur le code d'avant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJgsDSfsCbbn8cochFGwe2
…-sync

Bloc 99 — 🐛 Transfert Stuff → Paramètres joueur annulé tout seul (tampon de version dans les réglages)
…tion)

Numéroté 100 : le 99 est pris par le correctif du panneau joueur mergé
juste avant.

A. Nouveau champ « URL du script de suivi » dans l'onglet Configuration,
   volontairement générique — rien ne connaît l'outil derrière l'URL.
   Stocké dans une table `site_settings` (clé/valeur nommée) plutôt que
   dans `locale_settings`, qui est indexée par langue : les prochaines
   sections de l'onglet y écriront sans migration à chaque fois.
   La validation refuse tout ce qui n'est pas une URL http(s) analysable
   — y compris `javascript:`, `data:` et les URL portant des
   identifiants — puisque la valeur finit en <script src> sur toutes les
   pages du site. Champ vide = suivi désactivé, rien n'est chargé.

B. Injection dans le <head> du layout racine, donc public ET admin, avec
   le nonce de la requête. C'est ce qui rend une URL modifiable à chaud
   possible : la CSP est en `'strict-dynamic'`, où les listes blanches
   de domaines sont ignorées et où seul le nonce autorise un script.

   Point que le brief n'avait pas vu : le nonce autorise le CHARGEMENT,
   pas les envois de mesures, qui relèvent de `connect-src 'self'`. Un
   tracker sur un autre domaine était donc chargé puis muet. Le
   middleware qui construit la CSP tourne sur l'Edge et ne peut pas lire
   la base ; l'origine vient donc de la variable TRACKING_ORIGIN, passée
   par `new URL()` pour n'en garder que le schéma/hôte/port — ce qui
   interdit aussi d'injecter des directives supplémentaires. Vérifié :
   la variable est bien lue à l'exécution, pas figée au build.

C. Chaque section de l'onglet est un <details> à part : le pliage est
   une propriété du composant de section, pas un arbitrage entre deux
   panneaux connus. Indépendance structurelle, sémantique de divulgation
   et clavier gratuits, aucun composant client. Ouvertes par défaut, pour
   ne pas transformer l'onglet en pile d'en-têtes fermés.

Tests : 17 unitaires sur la validation d'URL et l'origine CSP (dont
l'injection de directive), 3 sur le pliage indépendant (clics réels,
jsdom bascule bien les <details>), 3 sur le panneau, 3 sur la CSP du
middleware, et 1 e2e qui fait le tour complet — sauvegarde admin, script
présent avec son nonce sur une page publique ET une page admin, nonce
autorisé par l'en-tête CSP servi, aucune violation signalée par le
navigateur, puis effacement du champ et script absent.

Contre-vérifié : sans nonce, le test rougit ; avec un nonce invalide, le
navigateur remonte bien `script-src-elem https://…/script.js`, donc
l'assertion « aucune violation » porte réellement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJgsDSfsCbbn8cochFGwe2
…mpose

P1 (fondé, et sérieux). Le rôle `admin` a `configuration.write` mais se
voit refuser `users.manage`, `logs.purge` et `content.*`. Or l'URL de
suivi devient du code exécuté dans cette origine, avec un nonce valide,
sur toutes les pages — y compris celles que charge un super admin. Un
`admin` pouvait donc faire exécuter le script de son choix pendant la
session d'un super admin et appeler l'API en son nom : exactement les
capacités que la matrice de rôles lui refuse.

Nouvelle capacité `configuration.scripts`, exclue du filtre `admin`,
donc super_admin seul. La route est gardée dessus (et non plus sur
`configuration.write`), et la section n'est pas rendue à un `admin` —
lui montrer un champ dont l'enregistrement échoue serait un piège. Le
reste de l'onglet Configuration ne change pas pour lui.

P2 (fondé). La valeur précédente était lue hors transaction : deux
sauvegardes concurrentes lisaient la même ancienne valeur et la seconde
consignait un diff d'audit depuis une valeur qu'elle n'avait pas
remplacée. Lecture déplacée dans la transaction.

P1 sur l'audit en français : pas retenu ici. `auditMessage` est
français par construction (sa table de verbes l'est) et les 12
appelants passent tous une cible française — c'est la convention de
tout le journal d'audit, pas quelque chose que ce bloc introduit. Le
corriger pour cette seule route rendrait le journal incohérent ;
le corriger partout est un bloc à part.

Demande utilisateur : TRACKING_ORIGIN est posée directement dans
docker-compose.yml, sans passer par .env — ce n'est pas un secret, elle
apparaît de toute façon dans l'en-tête CSP de chaque réponse. Elle reste
surchargeable depuis l'environnement.

Tests : la capacité est épinglée en unitaire (super_admin seul), et
l'e2e crée un compte `admin` réel, vérifie qu'il reçoit 403 sur la route
et que le champ ne lui est pas affiché. Contre-vérifié en ramenant la
garde à `configuration.write` : le test rougit avec 200 au lieu de 403.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJgsDSfsCbbn8cochFGwe2
Le vrai extrait fourni par Umami porte un `data-website-id` à côté du
`src` ; le champ du Bloc 100 ne produisait que le `src`, donc
l'intégration réelle ne mesurait rien.

Second champ « Identifiant du site » dans la même section, optionnel —
certains outils n'en demandent pas, et vide signifie « pas d'attribut »
plutôt que « attribut vide » : `undefined` en JSX ne rend rien du tout.
Même garde que l'URL (`configuration.scripts`, super admin seul), même
route, même transaction.

Validation : React échappe les valeurs d'attribut, donc rien ici ne peut
injecter de balisage même sans contrôle. Ce que le contrôle refuse, c'est
une valeur qui n'est pas un identifiant — guillemets, chevrons, accent
grave, espaces, caractères de contrôle, au-delà de 200 caractères. Un
admin qui colle la balise entière au lieu de l'identifiant l'apprend ici
plutôt que par un tracker muet. Le message d'erreur distingue les deux
champs, pour ne pas envoyer chercher au mauvais endroit.

Les deux clés sont lues en une seule requête (`getTrackingSettings`),
mémoïsée par requête HTTP : le layout racine s'exécute sur chaque page,
c'est le coût de la fonctionnalité sur chacune d'elles.

Tests : 13 unitaires sur la validation de l'identifiant, 2 sur le panneau
(envoi conjoint, message propre à l'identifiant refusé), et l'e2e vérifie
les deux attributs présents quand les deux champs le sont, `src` seul
quand l'identifiant est vide, et 400 sur une valeur qui n'en est pas un.
Contre-vérifié en retirant l'attribut du layout : l'e2e rougit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJgsDSfsCbbn8cochFGwe2
Bloc 100+101 — ⚙️ Suivi des visites configurable (URL + identifiant) + sections repliables
@magicgg91
magicgg91 merged commit 1a275c8 into main Sep 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants