Files
librodrome/actes/2026-10-26-persistance-messages.md
T
YvvandClaude Opus 5.5 4057f50bd3
ci/woodpecker/push/woodpecker Pipeline was successful
actes : persistance des messages — exécuté et vérifié en production
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>
2026-10-26 19:50:34 +01:00

274 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) :
```bash
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.
```bash
# É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
- [x] P7 et P8 rejoués : `200`, et le même nombre de messages publiés
- [x] `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`
- [x] La veille a vu partir l'ancien conteneur, et sa dernière relève est identique à la sauvegarde
- [x] Pipeline du commit `a6ba0a6662d5` : `success`
- [x] 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 » :
```bash
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 »).
## 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…`, P3 `a6ba0a6662d5…`, P4 `0`,
P5 `1`, P6 `57 success` / `56 success` / `55 success`, P7 `200`, P8 `1`.
Contrôle ajouté (relecture, m1) : `docker diff` du 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 -1` final, 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 volume `librodrome-data` sur `/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`, accueil `200`, messages `1`.
- **Étape 6** : `main` locale rebasée sur `a6ba0a6` sans conflit ; ce dossier (`52f0a00`)
devient `c32d88b`.
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.