Files
librodrome/actes/2026-10-26-persistance-messages.md
T
YvvandClaude Opus 5.5 c32d88bec1 actes : persistance des messages — diagnostic et dossier préparé
Diagnostic en lecture seule sur sonic (26/10) : le conteneur tourne
dans /app, les montages visent /src/data et /src/public (dossiers
vides de l'hôte), les messages vivent dans la couche du conteneur et
la graine est rejouée à chaque déploiement ; ADMIN_GIT_SYNC=false,
pas de .git, aucun commit [admin]. Rien perdu depuis le 29/09 ;
avant, invérifiable.

Dossier PRÉPARÉ (mot « déploie ») : sauvegarde et volume semé sur
sonic, veille de l'ancien conteneur, push de a6ba0a6
(fix/persistance-messages), vérification et redémarrage à froid.
Scripts déroulés en entier en local avec le vrai docker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:46:41 +01:00

16 KiB
Raw Blame History

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 : PRÉPARÉ (26/10/2026 vers 18h, par la session de diagnostic ; ni figé ni armé)

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/data devient le volume librodrome-data, et les messages survivent désormais aux déploiements, aux redémarrages et à docker compose down -v (volume external).
  • Les montages morts /src/data et /src/public disparaissent. 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/ et content/ restent perdues à chaque déploiement, les médias envoyés par /admin ne sont toujours pas servis, et ADMIN_GIT_SYNC reste à 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/data monté sur le volume librodrome-data, aucun autre montage, 0 ligne seed-messages, fichier du volume = sauvegarde, idem après docker 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.yml inchangé, et aucune ligne seed-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

  1. É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 sur LPK/librodrome (acte « accorde »), .git monté, ADMIN_GIT_SYNC=true en CI, et chaque édition devient un push, donc un déploiement ; (c) des volumes sur site/ et content/, ce qui fait deux vérités face à git (déconseillé).
  2. 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, readAsset et assets[id]). Un envoi n'est donc jamais servi. /admin/media lit /app/public : déduit du code, il doit répondre 500 (non sondé).
  3. Canon démenti : /DEV/AGENTS.md (Démarrage §3) et harness/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 pour CLAUDE.md du dépôt, § Données runtime : « Persisté entre les rebuilds — jamais écrasé ».
  4. Sauvegarde sur sonic : la garder, ou la supprimer après un déploiement suivant réussi (acte « supprime »).