blog.vfg.lol

Jah Messenger v4 : du brief à la prod en vingt-sept heures

Récit complet, du dépôt vide au premier /readyz en production — 27 septembre 2026


Jeudi 25 septembre, 21 h 43. FG colle dans une session Claude Code un brief de quatre-vingt-dix-sept mille caractères et un dépôt Git vide. Samedi 27 septembre, 0 h 26, https://jah.vfg.lol/readyz répond 4.0.0-alpha.1.

Entre les deux : 118 commits, 34 pull requests, 17 migrations de base, environ 43 000 lignes de JavaScript, de CSS et de tests, cinq dépendances en tout et pour tout, huit phases de produit, une revue de sécurité qui a trouvé une vraie faille critique, et un premier déploiement qui a raté deux fois avant de passer à la main.

Cet article raconte tout, dans l'ordre : ce que Jah veut être, comment le chantier a été découpé, ce que chaque phase a apporté, ce qui a cassé et pourquoi, comment plusieurs agents ont travaillé en même temps sans se marcher dessus, et ce qu'il reste à faire maintenant que c'est en ligne.


1. Ce qu'est Jah

Le brief s'ouvre sur une phrase qui a servi de boussole jusqu'au bout :

Jah est un MMORPG intime, mobile et web, pour des cercles de confiance : un petit monde persistant façon Habbo Hotel / Les Sims, piloté par le chat, habillé en MSN Messenger 2005. Le jeu est un prétexte ; ce qu'il produit, ce sont des rires partagés, des gestes réels et des souvenirs.

Et une devise : « MSN devant, MMORPG derrière, la vraie vie au bout. »

Concrètement, Jah est un messager. Il a une liste de contacts, des fenêtres de conversation bleues à coins arrondis, des pseudos colorés, et le Wizz, qui fait trembler la fenêtre. Derrière ces fenêtres vit un village : une Place, des maisons, un jardin, des salons qui deviennent des bâtiments, des chantiers que les habitants font avancer ensemble. Et au bout, la vraie vie : des quêtes qu'on ne valide qu'en se voyant pour de vrai, des rituels (la question du dimanche, les cartes d'anniversaire), et à terme une lampe qui s'allume dans le salon de FG quand quelqu'un lui fait un clin d'œil.

Dans l'écosystème VFG, Jah est la chaîne E8, le Refuge collectif, rendue jouable : l'endroit où d'autres humains entrent dans la boucle.

La boucle en six maillons

Le brief décrit chaque mécanique comme une boucle, et le STATUS du dépôt la vérifie maillon par maillon :

MaillonDans Jah
BesoinNe pas vivre et créer seul ; garder le lien avec la famille et les amis éloignés
ReprésentationUn village persistant : avatars, chambres, lieux, fils d'amitié, chantiers
Contrainte perceptibleAmbiante, jamais interruptive : une onde traverse la Place, le tableau des quêtes s'allume
ActionDans l'appli (jouer, écrire, construire) et surtout dans la vraie vie
PreuveLa confirmation de l'autre personne, et un souvenir daté
RécompenseUn objet-souvenir, une étincelle pour le village, un lieu qui s'ouvre

Une charte de jeu qui prime sur tout

Le cœur du brief, ce ne sont pas les fonctionnalités, c'est une charte de douze règles, chacune couverte par un test. Quelques-unes, pour donner le ton :

  • Réciprocité, pas volume. On compte des échanges (A écrit, B répond), jamais un nombre brut de messages. Spammer ne rapporte rien.
  • Rien ne se perd. Aucune série qui casse, aucun score qui baisse. Une relation calme « s'endort » et se réveille au premier échange.
  • Pas de pression de lecture. Aucun accusé de lecture visible par l'autre. MSN n'en avait pas.
  • Calme par défaut. Heures calmes de 22 h à 8 h ; aucun push pour un badge.
  • Pull par défaut. Jamais « X est en ligne », jamais « le village t'attend ».
  • Session terminable. Le rituel « Bonne nuit » couche ton avatar, résume ta journée en trois lignes et t'invite à fermer.
  • Jamais de faux vivant. Un village vide se montre vide ; toute rediffusion est étiquetée.

Et un test de conception, hérité de la charte facilitateur/aspirateur : « Ça rapproche au moins deux personnes ? Ça laisse un souvenir ? Ça aide à partir sans frustration ? » Deux « non », la fonctionnalité ne passe pas.

L'étoile polaire de Jah n'est pas le temps passé : ce sont les moments partagés par semaine, et la part des sessions closes par « Bonne nuit ». La durée de session sert à diagnostiquer un effet aspirateur, jamais à être maximisée.


2. Les règles du chantier

Le brief ne dit pas seulement quoi construire. Il dit comment, et ces règles ont tenu du premier au dernier commit.

Un ordre de priorité non négociable. En cas de conflit : sécurité et vie privée (§8), puis charte de jeu (§2), puis contraintes techniques (§4), puis tout le reste.

Des décisions réservées. Une liste (§14) de ce qui appartient à FG seul : secrets, DNS, serveur, ports, données réelles, contenus tiers, jetons des ponts, la lumière du salon, le ton du « Lundi » et du bal masqué, la date de mise en service, la composition des villages… Sur ces sujets, l'agent s'arrête et pose la question au lieu de contourner.

Tout le reste, l'agent le tranche seul, et l'écrit. Chaque décision non spécifiée va dans le CHANGELOG de la phase. À la fin de la phase 7, on en comptait déjà 98.

Cinq dépendances, pas une de plus. fastify, @fastify/websocket, @fastify/static, @fastify/cookie, @node-rs/argon2 ; et playwright pour les tests. Tout le reste est écrit à la main : la validation de schéma, le rate-limit, les migrations, le lint, la release, le Web Push (RFC 8291 et 8292 avec node:crypto), le nettoyage des métadonnées d'images, le QR code, le moteur du monde. Les dépendances ne bougeront plus : ajouter une bibliothèque est une décision de FG.

Des interdits vérifiés par la machine. Le lint maison refuse un .env suivi, une adresse IP publique, un chemin de lecteur Windows, un puits HTML (innerHTML et ses cousins), un fichier de plus de 600 lignes, et un échec masqué dans un script (|| true). Ce dernier nous a rattrapés dans la toute dernière PR.

Une porte de sortie par phase. Chaque phase se termine par npm run check vert, ses E2E verts, des captures en 1280×800 et en 390×844, un CHANGELOG technique, des notes utilisateur écrites « comme à des amis », et une PR avec sa liste de contrôle. Certaines phases ont aussi une porte « R2 », humaine : dix habitants qui ont dit « Bonne nuit », un premier chantier inauguré par le village, cinq quêtes IRL confirmées. Celles-là ne se constatent qu'avec de vrais habitants.


3. La chronologie

Heures de Paris.

QuandQuoiPR
jeu. 21 h 43Le brief, copié tel quel dans SPEC.md—
jeu. 22 h 38Phase 0 — Fondations#1
ven. 0 h 04Phase 1 — Cœur MSN#2
ven. 1 h 21Phase 2 — Salons et téléphone#3
ven. 2 h 23Phase 3 — La Bascule#4
ven. 3 h 45Phase 4 — Le monde#5
ven. 4 h 56Premier correctif des 30 images/s#6
ven. 5 h → 7 hLimite d'usage : la session principale est à l'arrêt ; d'autres prennent le relais#7 à #9
ven. 8 h 24Second correctif des 30 images/s, bug du validateur#10
ven. 8 h 44 → 9 h 29Phase 7 — Appels, en quatre étapes#11 à #14
ven. 9 h 49Phase 5 — Le village (menée en parallèle)#9
ven. 9 h 51 → 12 h 57Nouveautés, plan de la phase 6, CI plus sobre, journal de session#15 à #20
ven. 19 h 01Kit agentique .agents/#18
ven. 20 h 03 → 21 h 01Phase 8 : Ma page, visites, Jah Live#25, #27, #29, #30
ven. 20 h 07 → 22 h 40Phase 6 — La vraie vie, en cinq étapes#22, #23, #28, #31, #32
ven. 23 h 56Revue de tout le code : une faille critique, une trentaine de correctifs#33
sam. 0 h 17Tag v4.0.0-alpha.1—
sam. 0 h 26jah.vfg.lol répond—

Vingt-six heures et quarante-trois minutes entre le brief et la prod.


4. Phase par phase

Phase 0 — Fondations (55 minutes)

Sept commits posent tout le socle d'un coup :

  • La configuration refuse de démarrer sans ses secrets en production, et refuse de démarrer en préprod si le push ou les ponts sont allumés. Une préprod ne doit jamais contacter de vraies personnes.
  • SQLite (node:sqlite, tables STRICT) et un moteur de migrations avec empreintes : une migration déjà appliquée puis modifiée bloque le démarrage.
  • Deux écouteurs : l'appli (derrière Caddy) et l'ingress, la porte d'entrée des bots voisins, liée à la seule IP d'un réseau Docker interne.
  • /healthz et /readyz, des backups vérifiés (PRAGMA integrity_check sur la copie), une CSP stricte avec Trusted Types, le Dockerfile non-root, la CI, et une page de connexion MSN.

Le dépôt n'avait pas de branche main : première question à FG (« main = 1er commit »), puis PR #1, CI verte au premier passage. 139 tests, 9 E2E.

Trouvé en testant, avant même la première PR : le port de l'ingress répondait depuis le réseau Docker de Caddy. Il est depuis lié à l'IP fixe du seul réseau interne.

Phase 1 — Cœur MSN (1 h 26)

  • Invitations à usage unique (le jeton voyage dans le fragment de l'URL, la base n'en garde que l'empreinte) et un premier village.
  • Sessions en cookie __Host-, CSRF lié à la session, mots de passe argon2id.
  • Contacts, conversations privées, présence, et le Wizz avec son duel : Wizz rendu, combo, « DUEL LÉGENDAIRE ».
  • Un hub temps réel en WebSocket, avec reconnexion à délai croissant, et une petite admin.
  • Côté client : la liste de contacts, les fenêtres qu'on déplace, la zone de saisie, une boîte d'envoi hors ligne.

Première CI rouge. L'E2E du Wizz attendait le « NON ! » qui s'affiche quand on Wizze trop vite. Sur le runner de GitHub, il n'apparaissait jamais. Cause : le premier délai d'attente ne dure qu'un dixième de seconde, et sur une machine lente, le second clic arrivait après. Reproduit en local sur un seul cœur (taskset -c 0, une habitude qui ne nous a plus quittés). Correctif : le « NON » reste affiché 1,5 seconde, et le test clique quatre fois d'affilée. Les exigences du test n'ont pas bougé d'un millimètre : on corrige la cause, jamais le test.

Bugs trouvés en regardant l'appli tourner plutôt qu'en lisant le code :

  • un appareil révoqué qui se reconnectait à l'infini ;
  • « est en train d'écrire… » qui restait affiché ;
  • un champ mutual qui trahissait un blocage.

Ce dernier compte. Dans Jah, un blocage est invisible : la personne bloquée voit « hors ligne », rien d'autre.

Phase 2 — Salons et téléphone (1 h 07)

  • Salons façon IRC (#foot, #cuisine), avec leur sujet.
  • Réactions : 😂 ❤️ 😮 😢 👍 🦋 🔥 🤡.
  • Images nettoyées de leurs métadonnées (EXIF, GPS), GIF avec un cache côté serveur écrit contre la SSRF.
  • Appli installable (PWA) et Web Push chiffré, sans le texte du message par défaut.

Deuxième CI rouge : l'E2E du push ne voyait jamais la notification. Cause : Playwright lance par défaut un « headless shell » qui n'affiche pas les notifications d'un service worker ; en local, c'était un Chromium complet. Correctif : tous les E2E lancent le Chromium complet en mode headless.

Un écart au brief a été porté à FG plutôt que tranché seul : la CSP demandait trusted-types 'none', mais Chromium exige une politique pour enregistrer le service worker. Sans elle, ni push, ni installation. Une seule politique existe, jah-sw ; elle n'accepte que l'URL fixe /sw.js et ne crée jamais de HTML. FG l'a validée.

Phase 3 — La Bascule (57 minutes)

La phase qui prépare le passage de l'ancien MSN de la famille à Jah :

  • Import legacy : comptes, messages, fichiers. Les anciens mots de passe en salt:sha256 sont vérifiés à la connexion puis re-hachés en argon2id. C'était la deuxième question à FG, avec le pont Telegram.
  • Ingress v1 pour les bots voisins, et didou, le PNJ animateur qui prend la forme du papillon géant du village.
  • Pont Telegram via VFGate, codé mais éteint, opt-in. Jah ne parle jamais directement à l'API Telegram : deux processus en long-polling sur le même jeton se font éjecter, et c'est une leçon de VFG Hero.
  • Admin d'exploitation, notes de version dans l'appli (« 📰 Nouveautés »), QR sur les cartes d'invitation, export de ses souvenirs et suppression de compte (§8.13).
  • Une copie anonymisée de la base, pour une préprod qui ne contient jamais de données réelles.
  • Et, parce que le brief le demande en annexe : le code Konami et le bruit du modem 56k.

FG avait demandé à cette étape de vérifier que toutes les parties du plan étaient bien présentes. L'audit des phases 0 à 2 contre le SPEC n'a rien trouvé de manquant dans ces phases. Les manques venaient d'autres sections du brief et ont été comblés au passage. CI verte au premier passage.

Phase 4 — Le monde (1 h 11)

La phase la plus visible : le village apparaît sous les fenêtres MSN.

  • Un monde persistant, autoritaire côté serveur : les chemins, les murs, les lieux et les bulles sont tous validés par le serveur, jamais par le client.
  • Des avatars et leur compagnon papillon, des bulles au-dessus des têtes dans les lieux publics, des ondes anonymes quand un message privé part (on sait que ça vit, jamais qui parle à qui).
  • Des fils d'amitié visibles seulement si les deux personnes l'acceptent.
  • Des gestes à deux, le pouls du village, et le rituel « Bonne nuit ».
  • Des paliers d'amitié qui débloquent des styles de Wizz, des émoticônes, des clins d'œil animés et, au palier « Meilleurs amis », les memes Lulu : une petite boule rose aux grandes oreilles, dessinée nœud par nœud en SVG.
  • Un mode démo qui fait vivre soixante avatars sans serveur.

Troisième CI rouge, sur main cette fois. Le test de performance exige 60 avatars à 30 images par seconde sur un téléphone de milieu de gamme simulé (processeur ralenti 12 fois). Le runner mesurait 17 images/s. Premier correctif : découper la vue, mettre le ciel en cache, figer les images. On passe à 33 en local, mais à 29 sur le runner, pour un seuil de 30.

Là, la session a atteint sa limite d'usage, et s'est arrêtée près de deux heures.

Au retour, une trace du navigateur a montré le vrai coupable : la liste accessible du monde, reconstruite toutes les 400 ms, forçait une mise en page complète. Liste mise à jour sur place, images en ImageBitmap, translations arrondies au pixel : 24 → 33 images/s au ralenti, l'image la plus longue passe de 101 à 53 ms. Le seuil du test n'a jamais été baissé.

La nuit, et le travail en parallèle

Pendant que la session principale était à l'arrêt, FG a lancé d'autres agents :

  • une branche jetable pour prouver que la porte « CI rouge » bloque bien (#7, fermée sans fusion) ;
  • le déploiement derrière le Caddy systemd du serveur (#8) ;
  • la phase 5 entière (#9), sur sa propre branche.

La reprise, à 7 h 47, a commencé par un message de FG qu'on aurait pu graver dans le kit : « D'autres PR ont été faites en parallèle, vérifie avant de reprendre. »

L'état relevé : #6 fusionnée mais main toujours rouge ; #9 rouge sur le fuzzing (seize « erreurs internes »). La cause du rouge de #9 n'était pas dans #9, mais dans main depuis la phase 0 : le validateur de schéma maison descendait dans un champ « liste de valeurs » qui recevait un objet, et plantait. Correctif avec son test unitaire ; le fuzzing de #9 passe alors 25 fois sur 25. Décision : ne pas refaire la phase 5, déjà menée ailleurs, et ne rien pousser sur sa branche.

Phase 5 — Le village

Menée par une autre session, fusionnée à 9 h 49.

  • Des étincelles collectives, sans solde individuel : un moment partagé en produit, et on peut remonter de chaque étincelle au moment qui l'a fait naître.
  • Des chantiers votés par le village : l'Arcade et le Musée, choix de FG. À égalité, didou tire au sort. Les échafaudages montent, la fontaine jaillit, et l'inauguration donne une médaille « J'y étais ».
  • Des objets-souvenirs : chaque objet naît d'un moment réel (le duel de Wizz donne une foudre en bouteille, le fou rire une bulle de rire) et le raconte quand on le touche. On le pose dans sa chambre, on l'offre.
  • Des activités : morpion, gribouille, Puissance 4, Pierre-Feuille-Wizz, devine l'emoji.
  • Les souvenirs : le carnet d'une relation, le Musée des perles (un message n'est encadré qu'avec l'accord de son auteur), les capsules temporelles qui ne transmettent rien avant leur date, le journal du village, et les fantômes du jour : seul sur la Place, tu vois des silhouettes rejouer la journée, étiquetées « plus tôt aujourd'hui ». Jamais de faux vivant.

Phase 7 — Appels (1 h 06, en quatre PR)

FG a demandé de continuer « en local », une étape égale un commit, une PR et une fusion, avec un plan complet déposé et tenu à jour. Il a aussi demandé quelle phase était prise, pour en lancer une autre en parallèle. Réponse : la 7. La 5 était déjà prise, la 6 en dépendait, la 7 ne dépendait que des phases 1, 2 et 4.

  1. Signalisation 1-1 côté serveur : l'invitation, l'occupé indiscernable du refus, le blocage et l'invisible respectés, et 5 000 trames de fuzzing.
  2. L'appel dans le navigateur : audio, vidéo, partage d'écran, et une voix truquée (hélium, robot). Les E2E utilisent les faux micros, fausses caméras et faux écrans de Chromium, et vérifient que l'image et le son avancent vraiment des deux côtés.
  3. L'appel de salon en maillage : jusqu'à six personnes et quatre caméras, E2E à trois navigateurs.
  4. Le monde : en appel, ton avatar tient un combiné, et un fil relie les deux personnes, visible d'elles seules. Le push d'appel vit 60 secondes et se tait en heures calmes.

Le serveur TURN reste une décision de FG (§14) : sans lui, certains appels en 4G échoueront.

Phase 6 — La vraie vie (cinq étapes, en parallèle de la phase 8)

  • Quêtes IRL : coups de main et quêtes à deux, confirmés de façon croisée. On ne se valide jamais soi-même. Une quête non confirmée s'efface sans trace et sans rappel. Les photos de preuve restent lisibles des seuls membres.
  • Talents, nourris par la seule réciprocité : un Wizz rendu rapporte un point à celui qui l'avait envoyé, un monologue ne rapporte rien.
  • Rituels :
    • la question du dimanche : rien n'est révélé avant 20 h, pas même aux autres répondants ;
    • les cartes d'anniversaire collectives, invisibles de la personne fêtée jusqu'au jour J ;
    • « Il y a un an », la gazette de la semaine, toujours positive, et « Jah a N ans ».
  • Événements de monde :
    • des rassemblements : trois dans le même cercle sur la Place, et c'est une photo de famille pour chacun ;
    • l'album du village, la ruée vers la fontaine ;
    • Le Lundi, livré éteint : FG doit relire ses trois phrases avant de l'allumer ;
    • le cadavre exquis à trois.
  • Commandes #quete, #question, #bettermsn vers une boîte à idées que FG seul lit, et un Robot Crieur éteint, qui relaiera les nouvelles des projets voisins.

Tous les rituels sont testés avec une horloge simulée : on avance le temps de 20 h moins une minute à 20 h pile, on franchit les changements d'heure, et on vérifie ce que chacun voit.

Phase 8 — Bonus (en parallèle de la phase 6)

FG a choisi ses éléments : Ma page, visites entre villages, Jah Live, puis bal masqué et jam après la phase 6. Electron : pas maintenant. Les ponts vers la vraie vie (lumière, Hero, Signal Ledger) : codés, éteints.

  • Ma page, façon Skyblog assumé (·:¨¨:·) : le message perso, les badges, les perles des villages communs, et un livre d'or « Lâche tes com's » que son propriétaire modère. Une page interdite (autre village, blocage) répond exactement comme une page inexistante.
  • Les visites entre villages : sur invitation d'un contact, pour trois heures, la Place seulement. Les bâtiments n'y ont pas de nom, et le visiteur n'a accès à aucun historique. Tout le village voit le visiteur avec une valise 🧳. Lui ne voit que ceux qui se montrent à tous : un habitant avec le réglage par défaut reste invisible et inaudible pour un inconnu. Le brief est clair : « Peut inclure des enfants : rien ne doit les exposer à des inconnus. »
  • Jah Live, l'ancien Mode Soirée :
    • FG ouvre une soirée (un salon de 24 h) et tire des passes : un QR par invité, à usage unique ;
    • l'invité d'un soir entre avec son téléphone, un prénom suivi d'un ticket 🎟️ que personne ne peut retirer, et un avatar ;
    • la télé de la soirée projette les bulles qui montent, des rafales d'emojis à chaque réaction, l'écran qui tremble au Wizz, et les photos en polaroïds, seulement si leur auteur a coché « Sur le grand écran » ;
    • à la fin, les invités s'éteignent, le salon est rangé, et le livre d'or devient un objet-souvenir 📖 pour ceux du village qui y étaient.

Le point le plus délicat de Jah Live, c'est l'invité : un inconnu, par définition. Le brief dit : « Un invité d'un soir n'accède qu'à son salon. » On l'a traduit par une porte fermée par défaut :

  • côté API, seules cinq routes marquées guest lui sont ouvertes ;
  • côté temps réel, sept types de trames, refusés avant même la lecture de leurs données.

Un test parcourt toutes les routes de l'application (88 refusées le jour de sa fusion) et toutes les trames. Toute nouvelle route ouverte aux invités doit être ajoutée à sa liste, sinon il échoue. Puis chaque garde a été retirée une par une, pour vérifier qu'un test tombait à chaque fois.


5. Plusieurs agents, un seul dépôt

Jah n'a pas été écrit par un agent. Le git log compte trois signatures : FG (les fusions), des sessions Claude Code, et un agent Cursor. Le vendredi soir, jusqu'à trois ou quatre sessions travaillaient en même temps : la phase 6, la phase 8, puis la revue de code et l'exploitation.

Ce qui a marché :

  • Un dépôt qui porte l'état. STATUS, CHANGELOG, les plans de chaque phase et leurs journaux. Une session peut perdre tout son contexte (la session principale a subi quatre compactions et deux limites d'usage) et reprendre sans rien perdre.
  • Vérifier avant de reprendre. Chaque reprise commence par l'état réel : PR ouvertes, CI de main, branches des autres. Jamais par ce qu'on croit se souvenir.
  • Des fusions d'essai. Avant chaque poussée, on fusionne à blanc avec les PR ouvertes des autres sessions dans un worktree jetable, on lance le lint et les tests, puis on jette le worktree.
  • Des insertions loin des hunks des autres. Quand deux sessions touchent src/app.mjs, chacune ajoute ses lignes dans une zone différente. Les conflits disparaissent presque tous.
  • Un seul rédacteur à la fois pour STATUS et les CHANGELOG. Les docs de la phase 8 attendent que la phase 6 ait fini d'écrire les siens.
  • Un kit agentique (.agents/ : règles, compétences de boot, de découpage, de vérification, de livraison), fusionné en #18, pour que n'importe quel agent démarre de la même façon.

Ce qui a coincé :

  • Les numéros de migration. Trois fois, deux sessions ont pris le même : 0010, puis 0011, puis 0012. Et le moteur refuse, à raison, une migration plus ancienne que la base. La règle est maintenant écrite : une PR qui prend un numéro ne se fusionne qu'après celles qui ont les numéros d'avant. Jah Live côté serveur a attendu la PR des rituels ; FG l'a fusionnée trois minutes après.
  • Deux sessions dans le même worktree. Elles partagent la branche et l'index. Résultat : deux commits dont les titres ne correspondent pas tout à fait à leur contenu. L'arbre final était cohérent et testé, mais la leçon est notée : ne jamais faire git add -A quand une autre session peut écrire.
  • Les petites erreurs humaines d'agent. Une fusion GitHub refusée deux fois pour une empreinte de commit abrégée, puis mal recopiée : on passe désormais la sortie de git rev-parse, telle quelle. Un git checkout -- fichier qui restaurait depuis l'index au lieu du dernier commit. Un kit d'agents qui contenait lui-même des chaînes interdites par le lint, et a rendu main rouge.

6. La sécurité, de bout en bout

La sécurité et la vie privée priment sur tout, et ça se voit dans le code :

  • Tout est fermé par défaut. Toute route /api exige une session, et toute mutation un jeton CSRF. Une route publique doit être marquée comme telle.
  • Journaux sans contenu : ni message, ni jeton, ni cookie, et des IP hachées avec un sel tournant.
  • Push chiffrés, sans texte de message sauf aperçu choisi.
  • Le blocage est indiscernable d'une absence : un appel à quelqu'un qui vous bloque finit en « pas de réponse », comme un appel ignoré.
  • Aucune donnée réelle dans le dépôt ; la préprod travaille sur une copie anonymisée, et chaque phase a ajouté ses textes à la liste de ce qu'on anonymise.

Et malgré tout ça, la revue de tout le code a trouvé une faille critique. FG l'avait demandée le vendredi soir : six revues en parallèle, une par domaine, puis chaque constat vérifié dans le code, et reproduit par un test pour les graves.

La faille : l'API d'administration répondait sans session si l'on encodait une lettre du chemin. /%61pi/admin/overview au lieu de /api/admin/overview. La garde regardait l'URL brute (qui ne commence pas par /api/), le routeur la décodait (et trouvait la route admin). Reproduit : un 200 sans cookie, et un compte ordinaire qui lisait la liste des comptes. Corrigé : la garde décide maintenant d'après la route trouvée par le routeur. Jah n'était pas encore en ligne ; personne n'a pu en profiter.

Autour, six défauts hauts, tous corrigés :

  • un corps image/* de 5 Mio accepté sur n'importe quelle route, et parcouru octet par octet par le validateur ;
  • une partie de morpion qui continuait après un blocage ;
  • des étincelles sans plafond ;
  • un appel encore allumé, micro et caméra compris, après une déconnexion ;
  • les Nouveautés effacées à chaque démarrage de l'image Docker.

Et une trentaine de défauts moyens ou bas.


7. Les tests, et pourquoi ils ont tenu

AprèsUnitaires et intégrationE2E
Phase 01399
Phase 121825
Phase 226638
Phase 330044
Phase 432653
Phase 734259
Phase 6, étape 543877
Revue de code45877
Aujourd'hui46677, plus le test des 30 images/s

Quelques partis pris :

  • Des E2E à plusieurs navigateurs, avec un ordinateur et un téléphone côte à côte. Mamie écrit depuis son ordinateur, Tonton répond depuis son téléphone ; ce sont des personnages de test, jamais de vraies personnes. Chaque E2E vérifie aussi qu'aucune violation de CSP et aucune erreur de console n'est apparue.
  • Des captures à chaque phase, en 1280×800 et en 390×844, relues à l'œil. Elles ont trouvé des bugs que les tests ne voyaient pas : un double ★ dans un titre, des boutons qui passaient à la ligne, un bandeau qui cachait les boutons du monde sur téléphone, des photos affichées à la taille d'un timbre.
  • Une horloge simulée partout où le temps compte : rituels, capsules, visites, expiration des invités, plafonds du jour.
  • Du fuzzing sur les trames temps réel.
  • La preuve par la mutation. Pour chaque garde importante, on retire le correctif et on vérifie que le test échoue. Un test qui passe avec et sans le correctif ne prouve rien.
  • Le seul cœur. Chaque E2E nouveau tourne deux fois sous taskset -c 0 avant d'être poussé. C'est ainsi qu'on voit en local ce que le runner de GitHub verra.
  • Une CI sobre. Une PR de docs ne lance que le lint ; le CHANGELOG compte comme du code ; le test de performance tourne à part. Et depuis la revue, un « lint seul » ne peut plus compter comme un vert complet.

8. La mise en ligne

FG a tranché : un seul hôte, jah.vfg.lol, qui sert à la fois de terrain d'essai et de production ; pas de jah-pre pour l'instant. L'ancien MSN historique garde ses ports ; Jah publie en local sur le sien, derrière le Caddy du serveur. Le DNS, la clé de déploiement, le fichier d'environnement avec ses secrets générés sur le serveur, le certificat : c'est FG. Push et ponts : éteints.

Tag v4.0.0-alpha.1, et… le workflow Release est rouge.

  1. Première tentative : container jah-hub-1 is unhealthy, une seconde après le démarrage. La sonde de santé laisse vingt secondes de grâce : le hub n'a pas échoué à la sonde, il est tombé au lancement. C'est la signature d'une erreur de configuration. Mais le script de déploiement n'affichait pas le journal du hub, et la CI ne disait pas pourquoi.
  2. Deuxième tentative : Error response from daemon: Container … is restarting. Le script faisait un backup dans le hub avant de déployer (docker compose exec), et ce hub redémarrait en boucle. Le démon refusait, et le script s'arrêtait avant même de déployer la version qui aurait pu le réparer.

La remise en route s'est faite à la main sur le serveur. À 0 h 26, /readyz répond 4.0.0-alpha.1 et /ws sans cookie répond 401 : le hub est en ligne.

Le correctif est la dernière PR en date (#34), fusionnée dans la nuit :

  • le backup se fait dans le hub seulement s'il est vraiment en marche, sinon par un conteneur ponctuel sans réseau, en lecture seule, sur le même volume ;
  • si ce backup échoue, rien ne démarre : jamais de migration sans backup ;
  • sans trace de déploiement réussi, la version du hub en place sert de retour arrière ;
  • en cas d'échec, les quarante dernières lignes du journal du hub s'affichent dans la CI (un journal sans cookie, jeton ni contenu, par construction).

Un test joue le script contre un docker et un curl simulés, et le scénario de la prod échoue bien avec l'ancien script.

Petite ironie : la première version de ce correctif utilisait || true, et le lint maison, écrit en phase 0, l'a refusée. Un échec masqué dans un script, c'est exactement ce qui empêche de comprendre un déploiement raté.


9. Les chiffres

  • 27 heures du brief à la prod (26 h 43).
  • 118 commits jusqu'à la mise en ligne (120 avec le correctif du déploiement) et 34 pull requests : 32 fusionnées, et deux fermées sans fusion (la preuve que la porte « CI rouge » bloque, et un doublon).
  • 17 migrations, additives et réversibles.
  • ~15 000 lignes de serveur, ~16 000 d'interface, ~12 000 de tests.
  • 5 dépendances de production, 1 de développement.
  • 466 tests unitaires et d'intégration, 77 E2E, 1 test de performance.
  • 98 décisions prises seul et notées à la fin de la phase 7, bien davantage depuis (chaque étape de la phase 6 et de la phase 8 a ajouté les siennes).
  • 4 questions réservées posées à FG pendant la première nuit, 3 choix pour la phase 8.
  • 5 CI rouges pendant la construction, toutes corrigées à la racine. Aucun seuil de test baissé.

10. Ce qui a marché, ce qui a cassé, ce qui a fait rire

Ce qui a marché.

  • Le brief. Quatre-vingt-dix-sept mille caractères, c'est long, mais chaque ambiguïté y avait déjà sa règle de départage. Les décisions réservées ont évité les mauvaises surprises : l'agent ne choisit ni les secrets, ni le TURN, ni la date de mise en service.
  • La charte de jeu en tests. « Rien ne se perd » ou « réciprocité, pas volume » ne sont pas des slogans : ce sont des assertions.
  • « Une étape = un commit, une PR, une fusion. » Des PR petites, relisibles, chacune avec sa liste de contrôle et ses captures.
  • Le dépôt comme mémoire. Aucune session n'a jamais dû « se souvenir » : tout était écrit.

Ce qui a cassé.

  • Les runners lents, qui ont révélé deux vrais problèmes : un délai d'une fraction de seconde, un rendu trop coûteux.
  • Le validateur de schéma, écrit à la main en phase 0, avec un bug qui dormait jusqu'au fuzzing de la phase 5.
  • Les numéros de migration, pris en double par des sessions parallèles.
  • La faille du chemin encodé, qu'aucun test n'avait imaginée avant la revue.
  • Le premier déploiement, qui a buté sur son propre backup.

Ce qui a fait rire.

  • Le « NON ! » du Wizz, qu'un runner trop lent ne voyait jamais.
  • Le message d'erreur générique de Jah, « Oups, le papillon a trébuché. », lu des dizaines de fois dans les tests.
  • Le mode Kikoo avec son aperçu, et le livre d'or « Lâche tes com's » avec ses ·:¨¨:·.
  • Le code Konami, et le bruit du modem 56k, exigés par l'annexe du brief.
  • Un test de la copie anonymisée qui cherchait « 4521 » dans les octets bruts d'une base, où un sel aléatoire pouvait le contenir par hasard : la sentinelle est devenue « 4521-8867-3309 ».

11. Ce qu'il reste à faire

Urgent, côté exploitation :

  • Sur le serveur, écrire 4.0.0-alpha.1 dans le fichier de trace du déploiement s'il manque. Le script sait maintenant s'en passer, mais c'est plus sûr. Et vérifier derrière Caddy qu'un chemin encodé répond bien 401.
  • Décider d'une copie des sauvegardes hors du serveur, avant d'y mettre de vraies données : les backups quotidiens, vérifiés, dorment sur le même volume que la base.
  • La bascule : le backup de l'ancienne appli, l'import de l'historique de la famille (à blanc, puis pour de vrai), les cartes d'invitation, le rebranchement de l'ingress et de Telegram. Tant que ce n'est pas fait, Jah tourne sur une base vide et l'ancien MSN reste en service.
  • La soirée test, puis deux semaines de retours d'usage réel (des correctifs en 4.3.x), avant les portes R2.

Les décisions de FG :

  • le serveur TURN pour les appels ;
  • allumer Le Lundi et le Robot Crieur ;
  • les jetons des ponts de la phase 8 ;
  • le ton du bal masqué ;
  • recalibrer l'amitié et les étincelles après un mois d'usage ;
  • le vrai dessin de Lulu.

Le code qui reste :

  • les ponts de la phase 8 (lumière OSC via VFGate, pont Hero, Signal Ledger) : écrits et testés, pas encore livrés ;
  • le bal masqué et la jam ;
  • les docs de la phase 8 ;
  • un petit défaut connu du Tableau des quêtes : il limite à 60 avant de filtrer ce qu'on voit.

Les portes humaines. Dix habitants qui ont dit « Bonne nuit ». Un premier chantier inauguré par le village. Cinq quêtes IRL confirmées. Ce sont les seules qui comptent vraiment, et aucun test ne peut les franchir à notre place.


12. Leçons pour la prochaine fois

  1. Un brief qui tranche d'avance vaut mieux qu'un agent qui improvise. Un ordre de priorité, une liste de décisions réservées, et « note ta décision » pour tout le reste.
  2. Corriger la cause, jamais le test. Cinq CI rouges, cinq vraies causes, zéro seuil baissé.
  3. Le seul cœur ne ment pas. Tout ce qui passe sur un cœur passe sur le runner.
  4. Retirer la garde pour prouver le test. C'est rapide, et ça distingue un test utile d'un test décoratif.
  5. En parallèle, le dépôt est l'arbitre : fusions d'essai, un seul rédacteur de STATUS, et des numéros de migration réservés dans l'ordre.
  6. Un script de déploiement doit savoir réparer ce qu'il déploie, et dire pourquoi il échoue.
  7. La revue de tout le code, avant la mise en ligne, n'est pas un luxe. Elle a trouvé la seule faille qui aurait vraiment compté.

Jah est en ligne. Le village est vide, comme il se doit : il se montre vide. Il reste à y faire entrer la famille.

— VFG / Claude Code, nuit du 26 au 27 septembre 2026

source markdown

Concepts de cet article

gardeCaddyworktreeVFG HeroSignal LedgerVFGatecompagnonsSkyblog

Partage le plus de concepts avec