diff --git a/HANDOFF.md b/HANDOFF.md index 1f0c7f5..5d6834d 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -28,6 +28,7 @@ neuf : messages visiteurs re-semés depuis `site/messages.yml`, éditions /admin perdues. Rétablir les montages (`/app/...`) et décider du sort du git sync touche aux données de prod : à trancher par Yvv avant le prochain déploiement. + → **26/10/2026, vérifié sur sonic : confirmé** (jamais persisté ; rien perdu depuis le 29/09, avant invérifiable) — correctif `fix/persistance-messages` (a6ba0a6, local) et dossier `actes/2026-10-26-persistance-messages.md` (PRÉPARÉ, mot « déploie ») ; éditions /admin, médias et canon démenti : § Hors champ du dossier. **Ce qui tourne** : rien (serveur de dev sur 3000 arrêté). diff --git a/actes/2026-10-26-persistance-messages.md b/actes/2026-10-26-persistance-messages.md new file mode 100644 index 0000000..d87dffb --- /dev/null +++ b/actes/2026-10-26-persistance-messages.md @@ -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' /tmp/lb-veille.log 2>&1 &' &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" diff --git a/actes/annexes/2026-10-26-persistance-messages-avant.sh b/actes/annexes/2026-10-26-persistance-messages-avant.sh new file mode 100644 index 0000000..c655419 --- /dev/null +++ b/actes/annexes/2026-10-26-persistance-messages-avant.sh @@ -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' &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" diff --git a/actes/annexes/2026-10-26-persistance-messages-veille.sh b/actes/annexes/2026-10-26-persistance-messages-veille.sh new file mode 100644 index 0000000..df50236 --- /dev/null +++ b/actes/annexes/2026-10-26-persistance-messages-veille.sh @@ -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)" = "$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"