Section d'exécution du dossier (sorties relevées par orch, session dev-65), CLAUDE.md corrigé (volume librodrome-data sur /app/data, m4 de la relecture), HANDOFF : état en ligne et décisions restant à Yvv. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
19 KiB
Acte — persistance des messages de librodrome.org (volume nommé sur /app/data)
Classe : déploie (un volume créé et semé sur sonic, puis le push de main, qui met en ligne sans étape de test)
Projet : librodrome Date : 2026-10-26
État : EXÉCUTÉ le 26/10/2026, 19h38-19h44, par la session dev-65 (orch, sous
mandat de rôle, relecture indépendante inscrite) ; préparé vers 18h par la session de
diagnostic. Section d'exécution en fin de dossier.
Constat (diagnostic du 26/10/2026, en lecture seule)
| Fait | Preuve (26/10/2026, sauf date indiquée) |
|---|---|
| La prod tourne sur sonic | getent ahostsv4 librodrome.org → 161.97.174.60 (sonic, harness/conventions/infra.md) |
Un seul conteneur : lpk-librodrome-main-app-1, image lpk-librodrome-main-app (sha256:8fe50a5b7c65…), créé le 2026-09-29T06:59:03Z |
docker ps -a, docker inspect |
Répertoire de travail /app ; le serveur lit et écrit process.cwd()/data, donc /app/data |
docker inspect (WorkingDir=/app) ; server/utils/content.ts, server/plugins/seed-messages.ts |
Montages : deux binds seulement, …/LPK/librodrome/data → /src/data et …/LPK/librodrome/docker/public → /src/public, aucun volume nommé |
docker inspect (.Mounts, .HostConfig.Binds) |
Les sources de ces binds sont des dossiers vides de root sous /woodpecker/src/git.open.us.org/LPK/librodrome/, créés par le démon le 2026-08-12 à 00h58m36Z, la seconde où s'est terminé le pipeline #55. L'ancien chemin EHV/librodrome/ est vide lui aussi (créé le 23/03 à 03h10m44Z, fin du #36 à 03h10m45Z). |
ls -la --time-style=full-iso, find sur sonic ; API CI. Mécanisme : la CI lance compose depuis son espace de travail, et un chemin relatif y désigne un chemin de l'hôte qui n'existe pas (déjà noté en 8a38c86, 28/02). |
/app/data/messages.yml vit dans la couche du conteneur, et non dans un montage : 1099 octets, daté du 2026-09-29T06:59:05Z, sha256 c8afc7de…a6b8, identique à la graine site/messages.yml, avec 1 message (celui d'Yvv) |
docker exec … sha256sum, stat, grep -c ; docker diff → A /app/data/messages.yml |
| Graine rejouée au premier démarrage du conteneur actuel | docker logs : 2026-09-29T06:59:05Z [seed-messages] data/messages.yml initialisé depuis site/messages.yml, une seule fois. Les redémarrages des 12/10 et 18/10 (×4) ne l'ont pas rejouée : un redémarrage garde la couche, une recréation (un déploiement) la détruit. |
| Aucune édition /admin dans le conteneur actuel | docker diff : rien sous /app/site ni /app/content |
ADMIN_GIT_SYNC=false, pas de /app/.git, et aucun commit [admin] dans l'historique |
docker inspect (variable non secrète) ; ls -d /app/.git → absent ; git log --all --grep='^\[admin\]' → 0 |
Les secrets NUXT_ADMIN_PASSWORD et NUXT_ADMIN_SECRET sont définis ; les variables Umami sont vides |
docker inspect, longueur seulement (aucune valeur relevée) |
42 déploiements réussis sur main du 25/02 au 29/09 (#4 à #57) ; les derniers : #46 le 24/04, #50 le 24/07 (manuel, 10 s : recréation non vérifiée), #55 le 12/08, #56 et #57 le 29/09 |
https://ci.open.us.org/api/repos/1/pipelines (lecture anonyme, Woodpecker 3.19.0) |
Diagnostic : la persistance de la prod est cassée, et elle l'a toujours été.
Avant le 19/03 (b9e6b4a), les messages s'écrivaient dans site/messages.yml, dans
le conteneur ; depuis, dans /app/data, dans le conteneur aussi. Chaque déploiement
a donc remis les messages à zéro, puis à la graine à partir du 28/03 (f633940).
Les éditions /admin de site/ et content/ ont subi le même sort, et ne sont jamais
revenues dans git.
Pertes : depuis le 29/09, rien n'a été perdu (aucun message reçu, aucune édition). Avant cette date, ce qui a été perdu n'est pas vérifiable : les conteneurs détruits emportent leur couche, et les journaux n'en gardent pas trace (le journal de l'application commence le 29/09, celui de fabio le 07/10).
Intention
Monter les messages des visiteurs sur un volume nommé librodrome-data en
/app/data, pour qu'ils survivent aux déploiements. Il faut le faire maintenant :
au prochain push, tout message reçu d'ici là serait perdu. Le code est prêt sur la
branche locale fix/persistance-messages (a6ba0a6), et il a été répété en local.
Avant — prémisses vivantes
Sur le poste, dans /home/yvv/Documents/PROD/DEV/librodrome (attendu en commentaire) :
git status --short # P1 : vide
git fetch -q origin && git rev-parse origin/main # P2 : 90f77da49b1c77faf9807ba48740b0abd22aff46
# sinon ARRÊT : quelqu'un a poussé (pull --rebase, rebaser la branche, refaire le dossier)
git rev-parse fix/persistance-messages # P3 : a6ba0a6662d54e6ed42ef26ce9e1411e873a2484
git merge-base --is-ancestor origin/main a6ba0a6662d54e6ed42ef26ce9e1411e873a2484; echo $? # P4 : 0
git diff --name-only origin/main a6ba0a6662d54e6ed42ef26ce9e1411e873a2484 \
| grep -E '(^|/)data/|\.env|\.db$|\.enc$|\.key$|\.pem$|id_rsa|id_ed25519|IDENTIFIANTS'; echo $? # P5 : 1 (rien)
curl -4 -s 'https://ci.open.us.org/api/repos/1/pipelines?perPage=3' \
| python3 -I -c 'import sys,json; [print(p["number"], p["status"]) for p in json.load(sys.stdin)]' # P6 : « 57 success » en tête, rien en running ni pending
curl -4 -s -o /dev/null -w '%{http_code}\n' https://librodrome.org/api/health # P7 : 200 (server/api/health.ts ne fait que répondre)
curl -4 -s https://librodrome.org/api/messages \
| python3 -I -c 'import sys,json; print(len(json.load(sys.stdin)))' # P8 : 1 au 26/10 (lecture seule : server/api/messages/index.get.ts)
Pour P8, un autre nombre veut dire que des messages ont été publiés depuis : le noter, il doit se retrouver après.
Sur sonic, les prémisses sont vérifiées par le script de l'étape 1, qui s'arrête en
les nommant : conteneur en marche, WorkingDir=/app, /app/data non monté, volume
librodrome-data absent, dossier de sauvegarde absent, docker diff vide sous
/app/site et /app/content. Une édition /admin trouvée arrête l'acte : elle doit
d'abord passer dans git, sinon ce déploiement l'effacerait.
Contenu sortant (git log origin/main..a6ba0a6 --stat, relevé le 26/10) :
a6ba0a6 2026-10-26 docker : messages persistés dans un volume nommé monté sur /app/data
docker/docker-compose.dev.yml | 3 ++-
docker/docker-compose.yml | 16 ++++++++++++++--
23b1767 2026-10-26 claude.md : fiche et boucle de vérification fusionnées
CLAUDE.md | 76 +++… HANDOFF.md | 11 +++…
852a488 2026-09-29 handoff : bloc REPRISE (bascule quota 2026-09-29)
HANDOFF.md | 39 +++…
1c7cc45 2026-09-29 actes : og + favicon — exécuté et vérifié en production
actes/2026-09-29-og-logo-mise-en-ligne.md | 8 +++++---
Le diff du compose retire deux lignes, les montages morts ../data:/src/data et
./public:/src/public, et en ajoute une, data:/app/data, plus le bloc volumes:
(librodrome-data, external: true). Ce dossier n'est pas dans le push : il est
sur main en local, et partira avec le déploiement suivant.
Commandes exactes
Depuis le poste, dans /home/yvv/Documents/PROD/DEV/librodrome. Les scripts sont
dans actes/annexes/, et chacun se retire de /tmp à la fin.
# Étape 1 — sauvegarde, puis volume créé et semé (sonic ; le conteneur en service n'est pas touché)
scp -q -o BatchMode=yes actes/annexes/2026-10-26-persistance-messages-avant.sh yvv@161.97.174.60:/tmp/lb-avant.sh \
&& scp -q -o BatchMode=yes actes/annexes/2026-10-26-persistance-messages-veille.sh yvv@161.97.174.60:/tmp/lb-veille.sh \
&& scp -q -o BatchMode=yes actes/annexes/2026-10-26-persistance-messages-apres.sh yvv@161.97.174.60:/tmp/lb-apres.sh \
&& ssh -o BatchMode=yes yvv@161.97.174.60 'bash /tmp/lb-avant.sh' </dev/null
# attendu : « sauvegarde : N message(s), sha256 … », « volume librodrome-data semé : sha256 … » (même empreinte), « ÉTAPE 1 OK »
# Étape 2 — veille sur l'ancien conteneur, jusqu'à sa disparition (30 min au plus)
ssh -o BatchMode=yes yvv@161.97.174.60 'setsid nohup bash /tmp/lb-veille.sh >/tmp/lb-veille.log 2>&1 &' </dev/null
# Étape 3 — mise en ligne (push du SHA vérifié), puis attente du pipeline
for i in 1 2 3 4 5; do if git push -q origin a6ba0a6662d54e6ed42ef26ce9e1411e873a2484:main; then break; fi; sleep 10; done; git status -sb | head -1
for i in $(seq 1 40); do s=$(curl -4 -s -m 20 'https://ci.open.us.org/api/repos/1/pipelines?perPage=5' | python3 -I -c 'import sys,json; print(next((p["status"] for p in json.load(sys.stdin) if p["commit"].startswith("a6ba0a6662d5")), "absent"))'); echo "$(date +%T) $s"; case "$s" in success|failure|error|killed|declined) break;; esac; sleep 15; done
# attendu : « success » en 3 min environ (#56 : 3 min 18 s, #57 : 2 min 36 s)
# Étape 4 — vérification sur sonic : bascule, veille, redémarrage à froid
ssh -o BatchMode=yes yvv@161.97.174.60 'cat /tmp/lb-veille.log; bash /tmp/lb-apres.sh && rm -f /tmp/lb-veille.log' </dev/null
# attendu : « ancien conteneur remplacé », « veille : dernière relève = sauvegarde », deux fois « fichier du volume = sauvegarde », « ÉTAPE 4 OK »
# Étape 5 — vu de l'extérieur
curl -4 -s -o /dev/null -w '%{http_code}\n' https://librodrome.org/api/health
curl -4 -s -o /dev/null -w '%{http_code}\n' https://librodrome.org/
curl -4 -s https://librodrome.org/api/messages | python3 -I -c 'import sys,json; print(len(json.load(sys.stdin)))'
# attendu : 200, 200, le nombre relevé en P8
# Étape 6 — branche locale sur la tête déployée, puis compte rendu
git switch main && git rebase a6ba0a6662d54e6ed42ef26ce9e1411e873a2484
Si le pipeline échoue à l'étape 3 (par exemple sur external volume "librodrome-data" not found), l'ancien conteneur continue de servir, sans changement : la répétition
l'a montré. La veille s'arrête seule au bout de 30 min. On ne relance rien : on
rapporte.
Effet attendu
- librodrome.org est reconstruit par Woodpecker.
/app/datadevient le volumelibrodrome-data, et les messages survivent désormais aux déploiements, aux redémarrages et àdocker compose down -v(volumeexternal). - Les montages morts
/src/dataet/src/publicdisparaissent. Les dossiers vides de root sous/woodpecker/src/git.open.us.org/{EHV,LPK}/librodrome/restent sur l'hôte, sans effet ; les retirer demande root (hors acte). - Ce qui ne change pas : les éditions /admin de
site/etcontent/restent perdues à chaque déploiement, les médias envoyés par /admin ne sont toujours pas servis, etADMIN_GIT_SYNCreste àfalse(décisions d'Yvv, voir plus bas). - Fenêtre : entre la sauvegarde et la bascule (3 min environ), un message posté va dans l'ancien conteneur. La veille le relève toutes les 5 s, et l'étape 4 s'arrête s'il y en a un. Le risque résiduel tient en quelques secondes : la dernière relève, puis l'arrêt du conteneur. Le débit observé est de 0 message en 27 jours.
- Irréversibilité : réversible (voir Retour). Ni l'acte ni son retour ne suppriment le volume ni la sauvegarde.
Vérifications après
- P7 et P8 rejoués :
200, et le même nombre de messages publiés lb-apres.sh:/app/datamonté sur le volumelibrodrome-data, aucun autre montage, 0 ligneseed-messages, fichier du volume = sauvegarde, idem aprèsdocker restart- La veille a vu partir l'ancien conteneur, et sa dernière relève est identique à la sauvegarde
- Pipeline du commit
a6ba0a6662d5:success - Sauvegarde présente sur sonic :
~/librodrome-sauvegardes/2026-10-26/(700, fichiers 600 ;messages.yml,SHA256SUMS,ancien-conteneur.id, relève de la veille) - Au déploiement suivant, quel qu'il soit :
docker exec lpk-librodrome-main-app-1 sha256sum /app/data/messages.ymlinchangé, et aucune ligneseed-messages. C'est la preuve en vraie grandeur.
Retour
Retour du code, par un nouvel acte « déploie » :
git switch main && git revert --no-edit a6ba0a6662d54e6ed42ef26ce9e1411e873a2484 \
&& for i in 1 2 3 4 5; do if git push -q origin "$(git rev-parse HEAD)":main; then break; fi; sleep 10; done
Effet du retour, répété en local le 26/10 : le conteneur est recréé sans le volume
et re-semé depuis la graine. Le volume librodrome-data reste intact, avec tous les
messages reçus depuis la bascule ; redéployer le correctif les remonte.
Arrêt après l'étape 1, avant le push : rien à défaire en production, puisque le conteneur n'a pas changé. Le volume et la sauvegarde restent ; un second passage de l'étape 1 s'arrêtera sur « le volume existe déjà », ce qui est voulu. Les retirer est un acte « supprime » à part.
La sauvegarde de sonic contient de vraies données de visiteurs (auteurs, adresses). Elle reste sur sonic, et ne quitte pas le serveur, jusqu'à ce qu'Yvv décide ; sa suppression est un acte « supprime ».
Répétition (26/10/2026, sur le poste)
Faite avec le vrai docker (29.4.1, compose v5.1.3) et les noms exacts de la prod :
projet lpk-librodrome-main, conteneur lpk-librodrome-main-app-1, image locale
lpk-librodrome-main-app (node:22-bookworm-slim plus le .output, site/ et
content/ du dépôt, en couche d'image comme en prod). L'ancien conteneur venait de
l'ancien compose (main), la bascule du compose d'a6ba0a6, et HOME pointait sur un
faux dossier.
- Nominal : étape 1 OK (sauvegarde, puis volume semé à la même empreinte), veille arrêtée à la bascule, étape 4 OK avant et après le redémarrage à froid, 0 graine rejouée.
- Nouveau compose sans volume :
external volume "librodrome-data" not found, sortie 1, ancien conteneur intact (même identifiant). - Étape 1 rejouée après la bascule : ARRÊT « /app/data est déjà monté ».
- Édition /admin présente dans la couche : ARRÊT, aucun volume créé.
- Message posté dans l'ancien conteneur après la sauvegarde : relevé par la veille (2 messages), étape 4 en ARRÊT avec la consigne.
- Message posté après la bascule, puis
--force-recreate: conservé, sans graine ; le volume survit àdown -v; le retour à l'ancien compose re-sème sans toucher au volume. - Second déroulé, après correction de la veille (elle relevait par nom, et le nom
désigne le nouveau conteneur dès la bascule ; elle relève maintenant par
identifiant, et laisse
veille.fin), à 16h58-16h59Z :- étape 4 lancée avant le déploiement : ARRÊT « conteneur non recréé » ;
- message posté après la bascule : la dernière relève reste égale à la sauvegarde, l'étape 4 passe avec la NOTE « fichier du volume changé (2 messages) », avant et après le redémarrage ;
- message tardif dans l'ancien conteneur : ARRÊT, comme au premier déroulé.
- Non déroulés : la sortie de la veille au bout de 30 min sans bascule, et l'arrêt « la veille n'a pas constaté la bascule » (fenêtre de moins de 5 s entre la recréation et la fin de la veille).
- Répétition retirée du poste : conteneur, volume et image locale supprimés, ainsi
que
/tmp/lbacte(rien que du factice et une copie du build).
Différences avec la prod : node 22 au lieu de 23.11 (sans effet sur les montages ni
sur la graine), compose du poste au lieu de celui de l'image docker:dind de la CI,
et pas de proxy fabio devant le conteneur.
Hors champ — décisions d'Yvv
- Éditions /admin (
site/,content/) : perdues à chaque déploiement, jamais remontées dans git. Trois voies : (a) /admin en lecture seule en prod, et le contenu ne s'édite plus que par git ; (b) rétablir le git sync : un clone avec droit d'écriture surLPK/librodrome(acte « accorde »),.gitmonté,ADMIN_GIT_SYNC=trueen CI, et chaque édition devient un push, donc un déploiement ; (c) des volumes sursite/etcontent/, ce qui fait deux vérités face à git (déconseillé). - Médias /admin :
upload.post.tsécrit dans/app/public, qui n'existe pas dans l'image, et Nitro ne sert que le manifeste figé à la construction (.output/server/chunks/nitro/nitro.mjs,readAssetetassets[id]). Un envoi n'est donc jamais servi./admin/medialit/app/public: déduit du code, il doit répondre 500 (non sondé). - Canon démenti :
/DEV/AGENTS.md(Démarrage §3) etharness/conventions/git.md(« Début de session », point 1) disent que librodrome écrit du YAML en prod via /admin et git sync. C'est faux depuis l'origine (preuves ci-dessus). Même chose pourCLAUDE.mddu dépôt, § Données runtime : « Persisté entre les rebuilds — jamais écrasé ». - Sauvegarde sur sonic : la garder, ou la supprimer après un déploiement suivant réussi (acte « supprime »).
Exécution
Acte exécuté le 26/10/2026, 19h38-19h44, par la session dev-65 (orch, mandat de rôle
du 26-27/10). Relecture indépendante de a6ba0a6 et des scripts : un sous-agent neuf,
verdict « ok » à 18h48, inscrite dans state/relectures.json. Sorties relevées par
orch, puis consignées par la session librodrome-e9.
- Prémisses (19:38:45) : P1 vide, P2
90f77da49b1c…, P3a6ba0a6662d5…, P40, P51, P657 success/56 success/55 success, P7200, P81. Contrôle ajouté (relecture, m1) :docker diffdu conteneur, 0 entrée sous/app/(public|site|content). - Étape 1 (19:39:43-19:39:51) : « sauvegarde : 1 message(s), sha256 c8afc7de728fb9d57f6565a36b40f16fe7beb438768f8f9127183bea9569a6b8, 1099 2026-09-29 06:59:05.542963509 +0000 » ; « volume librodrome-data semé : sha256 c8afc7de…a6b8 » ; « ÉTAPE 1 OK ».
- Étape 2 : veille lancée vers 19h40.
- Étape 3 : boucle de push lancée seule. Écart au déroulé : sans le
; git status -sb | head -1final, que le garde refuse sous mandat.origin/main=a6ba0a6662d54e6ed42ef26ce9e1411e873a2484. Pipeline n° 58 : running à 19:42:41, success à 19:43:51. - Étape 4 (jusqu'à 19:44:14) : « ancien conteneur remplacé, constaté à
2026-10-26T18:43:46Z » ; « veille : dernière relève = sauvegarde
(2026-10-26T18:43:41Z) » ; bascule, puis redémarrage à froid, deux fois « fichier du
volume = sauvegarde (1 message(s)) » ; « ÉTAPE 4 OK ».
Contrôle indépendant (relecture, m2) : journal du conteneur lu (2 lignes), 0 ligne
seed-messages; seul montage, le volumelibrodrome-datasur/app/data; conteneur créé le 2026-10-26T18:43:43Z. - Sauvegarde sur sonic :
~/librodrome-sauvegardes/2026-10-26/(700, fichiers 600 :SHA256SUMS,ancien-conteneur.id,derniere-releve.date,messages-derniere-releve.yml,messages.stat,messages.yml,veille.fin)./tmp/lb-*retirés, aucune veille en cours. - Étape 5 (19:44:41) : health
200, accueil200, messages1. - Étape 6 :
mainlocale rebasée sura6ba0a6sans conflit ; ce dossier (52f0a00) devientc32d88b.
Mineurs de la relecture, non bloquants, à reprendre si l'on réutilise ces scripts :
m1 avant.sh ne regarde pas /app/public, où /admin écrit ses médias (paré à la
main, ci-dessus) ; m2 || true sur docker logs dans apres.sh (paré par le contrôle
indépendant) ; m3 pas de témoin de fin d'étape 1 ; m4 CLAUDE.md décrivait encore
../data:/src/data (corrigé dans ce commit) ; m5 les statuts blocked et skipped ne
sont pas prévus par la boucle de l'étape 3 ; m6 le dossier de sauvegarde est à retirer
à la main si l'étape 1 échoue.
Reste ouvert : la preuve en vraie grandeur au déploiement suivant (dernière case des Vérifications après), et les décisions du § Hors champ.