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 »).
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Acte 2026-10-26-persistance-messages — étape 4, sur sonic, sous yvv, après le déploiement.
# Vérifie la bascule, puis un redémarrage à froid, puis revérifie.
# Lancement : ssh yvv@161.97.174.60 'bash /tmp/lb-apres.sh' </dev/null
set -euo pipefail
C=lpk-librodrome-main-app-1
V=librodrome-data
D="$HOME/librodrome-sauvegardes/2026-10-26"
stop() { echo "ARRÊT : $*" >&2; exit 1; }
check() {
[ "$(docker inspect -f '{{.State.Running}}' "$C")" = true ] || stop "$C ne tourne pas"
src=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/app/data"}}{{.Type}} {{.Name}}{{end}}{{end}}' "$C")
[ "$src" = "volume $V" ] || stop "/app/data n'est pas le volume $V (trouvé : '$src')"
others=$(docker inspect -f '{{range .Mounts}}{{.Destination}} {{end}}' "$C")
[ "$others" = "/app/data " ] || stop "montages inattendus : $others"
s=$(docker logs "$C" 2>&1 | grep -c 'seed-messages' || true)
[ "$s" = 0 ] || stop "la graine a été rejouée ($s ligne(s) seed-messages)"
hc=$(docker exec "$C" sha256sum /app/data/messages.yml | cut -d' ' -f1)
nc=$(docker exec "$C" grep -c '^ - id:' /app/data/messages.yml || true)
if [ "$hc" = "$h" ]; then
echo " fichier du volume = sauvegarde ($nc message(s))"
else
echo " NOTE : le fichier du volume a changé depuis la sauvegarde ($nc message(s)) : message reçu après la bascule, à regarder"
fi
}
wait_up() {
for _ in $(seq 1 30); do
if docker exec "$C" node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))" 2>/dev/null; then
return 0
fi
sleep 2
done
stop "$C ne répond pas sur /api/health après 60 s"
}
[ -s "$D/ancien-conteneur.id" ] || stop "étape 1 non jouée"
old=$(cat "$D/ancien-conteneur.id")
h=$(cut -d' ' -f1 "$D/SHA256SUMS")
new=$(docker inspect -f '{{.Id}}' "$C")
[ "$new" != "$old" ] || stop "conteneur non recréé : le déploiement n'a pas eu lieu"
# La veille doit avoir vu l'ancien conteneur partir, sans message arrivé après la sauvegarde.
[ -s "$D/veille.fin" ] || stop "la veille n'a pas constaté la bascule (encore en cours ?) : attendre 10 s et relancer cette étape"
[ -s "$D/messages-derniere-releve.yml" ] || stop "aucune relève de la veille"
hl=$(sha256sum "$D/messages-derniere-releve.yml" | cut -d' ' -f1)
[ "$hl" = "$h" ] || stop "un message est arrivé dans l'ancien conteneur après la sauvegarde ($(cat "$D/derniere-releve.date")) : le reporter dans le volume avant toute autre chose (hors acte, décision d'Yvv)"
echo "veille : dernière relève = sauvegarde ($(head -1 "$D/derniere-releve.date"))"
echo "bascule :"
wait_up
check
echo "redémarrage à froid :"
docker restart "$C" >/dev/null
wait_up
check
rm -f -- "$0"
echo "ÉTAPE 4 OK"
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Acte 2026-10-26-persistance-messages — étape 1, sur sonic, sous yvv.
# Prémisses, sauvegarde des messages, volume librodrome-data créé et semé.
# Ne touche pas au conteneur en service (lectures seulement).
# Lancement : ssh yvv@161.97.174.60 'bash /tmp/lb-avant.sh' </dev/null
set -euo pipefail
C=lpk-librodrome-main-app-1
IMG=lpk-librodrome-main-app
V=librodrome-data
D="$HOME/librodrome-sauvegardes/2026-10-26"
stop() { echo "ARRÊT : $*" >&2; exit 1; }
# --- Prémisses
[ "$(docker inspect -f '{{.State.Running}}' "$C")" = true ] || stop "$C ne tourne pas"
[ "$(docker inspect -f '{{.Config.WorkingDir}}' "$C")" = /app ] || stop "WorkingDir de $C différent de /app"
m=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/app/data"}}{{.Type}}{{end}}{{end}}' "$C")
[ -z "$m" ] || stop "/app/data est déjà monté ($m) : l'acte a déjà été joué ?"
if docker volume inspect "$V" >/dev/null 2>&1; then stop "le volume $V existe déjà"; fi
[ ! -e "$D" ] || stop "$D existe déjà"
dd=$(docker diff "$C")
modifs=$(printf '%s\n' "$dd" | grep -E '^[ACD] /app/(site|content)(/|$)' || true)
if [ -n "$modifs" ]; then
echo "$modifs" >&2
stop "éditions /admin présentes dans $C et absentes de git : les reporter dans git avant de déployer"
fi
# --- Sauvegarde (reste sur sonic, dossier 700, fichiers 600)
umask 077
mkdir -p "$D"
docker inspect -f '{{.Id}}' "$C" > "$D/ancien-conteneur.id"
docker exec "$C" cat /app/data/messages.yml > "$D/messages.yml"
docker exec "$C" stat -c '%s %y' /app/data/messages.yml > "$D/messages.stat"
[ -s "$D/messages.yml" ] || stop "sauvegarde vide"
n=$(grep -c '^ - id:' "$D/messages.yml" || true)
h=$(sha256sum "$D/messages.yml" | cut -d' ' -f1)
echo "$h messages.yml" > "$D/SHA256SUMS"
echo "sauvegarde : $n message(s), sha256 $h, $(cat "$D/messages.stat")"
# --- Volume créé et semé avec la sauvegarde
docker volume create "$V" >/dev/null
docker run --rm -i --entrypoint sh -v "$V":/data "$IMG" -c 'cat > /data/messages.yml' < "$D/messages.yml"
hv=$(docker run --rm --entrypoint sha256sum -v "$V":/data:ro "$IMG" /data/messages.yml | cut -d' ' -f1)
[ "$hv" = "$h" ] || stop "empreinte du volume ($hv) différente de la sauvegarde ($h)"
echo "volume $V semé : sha256 $hv"
rm -f -- "$0"
echo "ÉTAPE 1 OK"
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Acte 2026-10-26-persistance-messages — étape 2, sur sonic, sous yvv, en arrière-plan.
# Relève toutes les 5 s le fichier de messages de l'ancien conteneur, jusqu'à ce
# que le déploiement le remplace (ou 30 min au plus). Lectures seulement, sauf
# l'écriture de la relève dans le dossier de sauvegarde.
# Lancement : ssh yvv@161.97.174.60 'setsid nohup bash /tmp/lb-veille.sh >/tmp/lb-veille.log 2>&1 &' </dev/null
set -uo pipefail
C=lpk-librodrome-main-app-1
D="$HOME/librodrome-sauvegardes/2026-10-26"
[ -s "$D/ancien-conteneur.id" ] || { echo "ARRÊT : étape 1 non jouée"; exit 1; }
old=$(cat "$D/ancien-conteneur.id")
umask 077
i=0
while [ "$(docker inspect -f '{{.Id}}' "$C" 2>/dev/null)" = "$old" ] && [ "$i" -lt 360 ]; do
# par l'identifiant : après la bascule, le nom désigne déjà le nouveau conteneur
if docker exec "$old" cat /app/data/messages.yml > "$D/.releve.tmp" 2>/dev/null && [ -s "$D/.releve.tmp" ]; then
mv "$D/.releve.tmp" "$D/messages-derniere-releve.yml"
date -u +%FT%TZ > "$D/derniere-releve.date"
fi
i=$((i + 1))
sleep 5
done
rm -f "$D/.releve.tmp"
if [ "$i" -ge 360 ]; then
echo "FIN SANS BASCULE : 30 min écoulées, l'ancien conteneur tourne toujours"
else
date -u +%FT%TZ > "$D/veille.fin"
echo "ancien conteneur remplacé, constaté à $(cat "$D/veille.fin")"
fi
rm -f -- "$0"