Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.