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>
This commit is contained in:
Yvv
2026-10-26 19:46:41 +01:00
co-authored by Claude Opus 5.5
parent a6ba0a6662
commit c32d88bec1
5 changed files with 370 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
# 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) :
```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
- [ ] 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 » :
```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 »).