From 409a02561d853dd55a541372a4bece0a6993d276 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 10 Oct 2026 19:31:35 +0200 Subject: [PATCH] feat(deploy): Wiederherstellung proben und Skript dafuer (#045) Einer der vier Punkte bis 1.0.0: eine Sicherung einmal wirklich zurueckspielen. Mit einem eigenen Stapel durchgespielt - Datenstand festgehalten, Marke gesetzt, PDF abgelegt, gesichert, alle Volumes zerstoert, zurueckgespielt. Datenbank, PDF und Anmeldung waren zurueck. Zwei Luecken kamen erst dabei heraus: 1. Ohne --clean liess sich die Sicherung nur in eine LEERE Datenbank zurueckspielen - im Ernstfall also gerade nicht. Jetzt pg_dump --clean --if-exists, gegen eine Datenbank mit Daten geprueft. 2. Fuer die hochgeladenen PDFs gab es keinen beschriebenen Weg zurueck. deploy/restore.sh haelt die Dienste an, spielt Datenbank und PDFs zurueck, startet wieder und prueft die Erreichbarkeit. Die .env bleibt unangetastet: Sie traegt APP_SECRET, und ein falscher Wert dort macht gespeicherte Zugangsdaten unlesbar. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 12 ++ DEVLOG.md | 8 +- deploy/backup.sh | 5 +- deploy/restore.sh | 127 +++++++++++++++++++ docs/betrieb/sicherung.md | 50 ++++++-- docs/issues/045-wiederherstellung-geprobt.md | 40 ++++++ docs/issues/README.md | 3 +- docs/roadmap.md | 4 +- package.json | 2 +- 9 files changed, 234 insertions(+), 17 deletions(-) create mode 100755 deploy/restore.sh create mode 100644 docs/issues/045-wiederherstellung-geprobt.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 09010bb..42dcd79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,18 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Sicherheit +## [0.27.0] – 2026-10-10 + +### Hinzugefügt +- `deploy/restore.sh`: spielt Datenbank **und** hochgeladene PDFs aus einer Sicherung zurück, startet die Dienste und prüft die Erreichbarkeit (#045) + +### Behoben +- **Sicherungen ließen sich nur in eine leere Datenbank zurückspielen** – im Ernstfall also gerade nicht. `pg_dump` läuft jetzt mit `--clean --if-exists` (#045) +- Für die hochgeladenen PDFs gab es keinen beschriebenen Weg zurück (#045) + +### Dokumentation +- `docs/betrieb/sicherung.md` beschreibt den **geprobten** Ablauf: Daten zerstört, zurückgespielt, geprüft (#045) + ## [0.26.1] – 2026-10-10 ### Sicherheit diff --git a/DEVLOG.md b/DEVLOG.md index 1bac473..565ca68 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,7 +2,7 @@ ## Zeitübersicht -**Gesamt:** 24 h 38 min +**Gesamt:** 25 h 28 min | Issue | Zeit | |---|---| @@ -50,6 +50,7 @@ | #042 QDevice im Cluster | 0 h 20 min | | #043 Laufzeiten bearbeiten | 1 h 05 min | | #044 Eigenbedarf | 0 h 40 min | +| #045 Wiederherstellung geprobt | 0 h 50 min | | Meilenstein | Zeit | |---|---| @@ -72,7 +73,7 @@ ## 2026-09-17 · Sitzung 1 · 19:36–laufend **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) -**Version:** 0.26.1 +**Version:** 0.27.0 ### Erledigt - Auftrag gelesen, Rückfragen gestellt @@ -228,6 +229,9 @@ - `npm audit` turnusmäßig geprüft: zwei neue hohe Meldungen (`sharp`/librsvg, `source-map-js`). Beide angehoben, Tests und Build grün - Produktivinstanz antwortet: `https://proxledger.wilde-ste.de/api/gesundheit` meldet Datenbank erreichbar +- Wiederherstellung geprobt (#045): Daten zerstört, zurückgespielt, geprüft. Zwei Lücken kamen erst dabei heraus – ohne `--clean` ließ sich nur in eine leere Datenbank zurückspielen, und für die PDFs gab es gar keinen Weg +- Damit ist einer der vier Punkte bis 1.0.0 erledigt. Offen bleiben PMG, PBS mit Lizenz und ein vollständiger Lizenzjahr-Durchlauf (17.10.) + ### Nächster Schritt - #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline - Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023) diff --git a/deploy/backup.sh b/deploy/backup.sh index a8a41af..52982de 100755 --- a/deploy/backup.sh +++ b/deploy/backup.sh @@ -30,7 +30,8 @@ echo "Sichere ProxLedger (${ART}) nach ${ZIEL} …" if [ "$ART" = "docker" ]; then echo "Sichere Datenbank …" - docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-proxledger}" "${POSTGRES_DB:-proxledger}" \ + docker compose exec -T postgres pg_dump --clean --if-exists \ + -U "${POSTGRES_USER:-proxledger}" "${POSTGRES_DB:-proxledger}" \ | gzip > "${ZIEL}/datenbank-${STAND}.sql.gz" echo "Sichere hochgeladene Dateien …" @@ -48,7 +49,7 @@ else } echo "Sichere Datenbank …" - pg_dump "$DATABASE_URL" | gzip > "${ZIEL}/datenbank-${STAND}.sql.gz" + pg_dump --clean --if-exists "$DATABASE_URL" | gzip > "${ZIEL}/datenbank-${STAND}.sql.gz" echo "Sichere hochgeladene Dateien …" UPLOADS="${UPLOAD_DIR:-${VERZEICHNIS}/data/uploads}" diff --git a/deploy/restore.sh b/deploy/restore.sh new file mode 100755 index 0000000..cc81c5a --- /dev/null +++ b/deploy/restore.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# ProxLedger aus einer Sicherung wiederherstellen (#046). +# +# bash deploy/restore.sh /var/backups/proxledger # jüngste Sicherung +# bash deploy/restore.sh /var/backups/proxledger 20261010-192437 +# bash deploy/restore.sh … --ja # ohne Rückfrage +# +# Spielt Datenbank und hochgeladene PDFs zurück. Die `.env` wird **nicht** angefasst: +# Sie trägt APP_SECRET, und ein falscher Wert dort macht gespeicherte Zugangsdaten unlesbar. +# Die passende Fassung liegt als env-.txt in der Sicherung – im Zweifel von Hand vergleichen. + +set -o errexit +set -o nounset +set -o pipefail + +VERZEICHNIS=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$VERZEICHNIS" + +blau() { printf '\033[1;36m› %s\033[0m\n' "$*"; } +gruen() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; } +fehler() { + printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2 + exit 1 +} + +QUELLE="${1:-/var/backups/proxledger}" +STAND="${2:-}" +[ "${2:-}" = "--ja" ] && STAND="" +OHNE_RUECKFRAGE="nein" +for argument in "$@"; do [ "$argument" = "--ja" ] && OHNE_RUECKFRAGE="ja"; done + +[ -d "$QUELLE" ] || fehler "Kein Sicherungsordner unter ${QUELLE}." + +# Jüngste Sicherung nehmen, wenn kein Stand angegeben ist. +if [ -z "$STAND" ]; then + DATENBANK=$(ls -1t "${QUELLE}"/datenbank-*.sql.gz 2>/dev/null | head -n1 || true) +else + DATENBANK="${QUELLE}/datenbank-${STAND}.sql.gz" +fi + +[ -n "${DATENBANK:-}" ] && [ -f "$DATENBANK" ] || fehler "Keine Datenbanksicherung in ${QUELLE} gefunden." +UPLOADS="${DATENBANK/datenbank-/uploads-}" +UPLOADS="${UPLOADS/.sql.gz/.tar.gz}" + +# Siehe deploy/update.sh – der Dienst ist das eindeutigere Merkmal. +if systemctl cat proxledger.service >/dev/null 2>&1; then + ART="dienst" +else + ART="docker" +fi + +printf '\033[1;33m! Achtung: Der aktuelle Datenbestand wird durch die Sicherung ersetzt.\033[0m\n' +echo " Installationsart: ${ART}" +echo " Datenbank: $(basename "$DATENBANK")" +echo " Uploads: $([ -f "$UPLOADS" ] && basename "$UPLOADS" || echo 'keine in dieser Sicherung')" + +if [ "$OHNE_RUECKFRAGE" != "ja" ]; then + read -r -p "Wirklich zurückspielen? [tippen Sie ja] " antwort + [ "$antwort" = "ja" ] || fehler "Abgebrochen." +fi + +# ---------------------------------------------------------------- Datenbank +blau "Datenbank zurückspielen …" +if [ "$ART" = "docker" ]; then + # Web und Worker anhalten: Sie würden während des Einspielens weiterschreiben. + docker compose stop web worker >/dev/null 2>&1 || true + docker compose up -d postgres >/dev/null + # Kurz warten, bis Postgres Verbindungen annimmt. + for _ in $(seq 1 30); do + docker compose exec -T postgres pg_isready -U "${POSTGRES_USER:-proxledger}" >/dev/null 2>&1 && break + sleep 1 + done + gunzip -c "$DATENBANK" | docker compose exec -T postgres psql -U "${POSTGRES_USER:-proxledger}" \ + -d "${POSTGRES_DB:-proxledger}" >/dev/null +else + systemctl stop proxledger proxledger-worker >/dev/null 2>&1 || true + # shellcheck disable=SC1091 + set -o allexport + . ./.env + set +o allexport + [ -n "${DATABASE_URL:-}" ] || fehler "DATABASE_URL fehlt in der .env." + gunzip -c "$DATENBANK" | psql "$DATABASE_URL" >/dev/null +fi +gruen "Datenbank zurückgespielt." + +# ---------------------------------------------------------------- Uploads +if [ -f "$UPLOADS" ]; then + blau "Hochgeladene PDFs zurückspielen …" + if [ "$ART" = "docker" ]; then + # Das Archiv enthält den Pfad /data/uploads – deshalb nach / entpacken. + docker compose run --rm -T --entrypoint tar web -xzf - -C / < "$UPLOADS" + else + ZIEL_UPLOADS="${UPLOAD_DIR:-${VERZEICHNIS}/data/uploads}" + mkdir -p "$(dirname "$ZIEL_UPLOADS")" + tar -xzf "$UPLOADS" -C "$(dirname "$ZIEL_UPLOADS")" + fi + gruen "PDFs zurückgespielt." +fi + +# ---------------------------------------------------------------- Starten und prüfen +blau "Dienste starten …" +if [ "$ART" = "docker" ]; then + docker compose up -d >/dev/null +else + systemctl start proxledger proxledger-worker +fi + +PORT="${PORT:-3000}" +for versuch in $(seq 1 45); do + if [ "$ART" = "docker" ]; then + erreichbar=$(docker compose exec -T web wget -qO- "http://127.0.0.1:3000/api/gesundheit" >/dev/null 2>&1 && echo ja || echo nein) + else + erreichbar=$(curl -fsS -o /dev/null --max-time 2 "http://localhost:${PORT}/api/gesundheit" && echo ja || echo nein) + fi + + if [ "$erreichbar" = "ja" ]; then + gruen "ProxLedger läuft wieder (nach ${versuch} s)." + echo + echo "Bitte jetzt anmelden und stichprobenartig prüfen, ob die Daten vollständig sind." + echo "Passt das Passwort nicht, stammt die Sicherung aus einer Zeit vor der letzten Änderung –" + echo "dann hilft: npm run benutzer -- passwort " + exit 0 + fi + sleep 1 +done + +fehler "Das Dashboard antwortet nicht. Protokoll ansehen: docker compose logs -n 50 web" diff --git a/docs/betrieb/sicherung.md b/docs/betrieb/sicherung.md index 33daad5..70d0071 100644 --- a/docs/betrieb/sicherung.md +++ b/docs/betrieb/sicherung.md @@ -27,19 +27,51 @@ ruft es als Erstes auf und bricht ab, wenn es fehlschlägt. ## Zurückspielen ```bash -# Docker -docker compose down -docker compose up -d postgres -gunzip -c sicherung.sql.gz | docker compose exec -T postgres psql -U proxledger proxledger +bash deploy/restore.sh /var/backups/proxledger # jüngste Sicherung +bash deploy/restore.sh /var/backups/proxledger 20261010-192437 # bestimmter Stand +``` + +Das Skript hält Dashboard und Worker an, spielt Datenbank **und** hochgeladene PDFs zurück, startet +alles wieder und prüft, ob das Dashboard antwortet. Vorher fragt es nach; `--ja` überspringt die +Rückfrage (für Skripte). + +Die `.env` fasst es bewusst **nicht** an: Sie trägt `APP_SECRET`, und ein falscher Wert dort macht die +gespeicherten Zugangsdaten für SMTP und Webhook unlesbar. Die zum Stand passende Fassung liegt als +`env-.txt` in der Sicherung – im Zweifel von Hand vergleichen. + +### Von Hand, wenn es sein muss + +```bash +docker compose stop web worker +gunzip -c datenbank-20261010-192437.sql.gz | docker compose exec -T postgres psql -U proxledger proxledger +docker compose run --rm -T --entrypoint tar web -xzf - -C / < uploads-20261010-192437.tar.gz docker compose up -d ``` -Danach die `.env` mit **demselben** `APP_SECRET` zurücklegen wie zum Zeitpunkt der Sicherung. Ein -anderer Wert lässt die gespeicherten Zugangsdaten unlesbar werden; das Dashboard sagt das beim -nächsten Versand deutlich, aber die Werte müssen dann neu eingetragen werden. +Bei der Installation als Dienst stattdessen `psql "$DATABASE_URL"` und `tar -xzf … -C /opt/proxledger/data`. + +## Geprobt am 10.10.2026 + +Nicht behauptet, sondern durchgespielt – mit einem eigenen Stapel: + +1. Stand festgehalten (9 Hosts, 4 Kunden, 10 Laufzeiten), ein Kunde zur Wiedererkennung umbenannt, + eine PDF-Datei abgelegt +2. Sicherung angelegt +3. **Alle Daten zerstört** (`docker compose down -v` – Datenbank und Upload-Volume weg) +4. `deploy/restore.sh` aufgerufen + +Ergebnis: Datenbank vollständig zurück (die umbenannte Kundin war wieder da), die PDF-Datei lag mit +den richtigen Rechten im Upload-Ordner, Anmeldung mit dem alten Passwort funktionierte. + +Dabei zwei Lücken gefunden und geschlossen: Die Sicherung ließ sich nur in eine **leere** Datenbank +zurückspielen (jetzt `pg_dump --clean --if-exists`), und für die hochgeladenen PDFs gab es überhaupt +keinen beschriebenen Weg. + +> Geprobt wurde der Docker-Weg. Für die Installation als Dienst ist derselbe Ablauf im Skript +> vorgesehen, aber noch nicht unter echten Bedingungen gelaufen. ## Das Wichtigste **Eine Sicherung, die nie zurückgespielt wurde, ist keine Sicherung.** Spielen Sie sie einmal in eine -leere Testumgebung ein und melden Sie sich dort an. Erst dann wissen Sie, dass Sicherung und -`APP_SECRET` zusammenpassen. +Testumgebung ein und melden Sie sich dort an. Erst dann wissen Sie, dass Sicherung und `APP_SECRET` +zusammenpassen. diff --git a/docs/issues/045-wiederherstellung-geprobt.md b/docs/issues/045-wiederherstellung-geprobt.md new file mode 100644 index 0000000..b1917ac --- /dev/null +++ b/docs/issues/045-wiederherstellung-geprobt.md @@ -0,0 +1,40 @@ +# #045 Wiederherstellung proben und Skript dafür +**Status:** erledigt +**Meilenstein:** v0.27.0 +**Labels:** deployment, doku +**Geschätzt:** 2 h **Tatsächlich:** 0 h 50 min + +## Ziel +Einer der vier Punkte bis 1.0.0: eine Sicherung **einmal wirklich zurückspielen**. Bis dahin war nur +geprüft, dass `deploy/backup.sh` Dateien schreibt. + +## Ablauf der Probe +Mit einem eigenen Docker-Stapel, getrennt von allen anderen Daten: + +1. Datenstand festgehalten (9 Hosts, 4 Kunden, 10 Laufzeiten), einen Kunden zur Wiedererkennung in + `Marke-192641` umbenannt, eine PDF-Datei in den Upload-Ordner gelegt +2. Sicherung angelegt +3. **Alle Daten zerstört** – `docker compose down -v`, also Datenbank- und Upload-Volume weg +4. Zurückgespielt und geprüft + +**Ergebnis:** Datenbank vollständig zurück (die Marke war wieder da), die PDF-Datei lag mit den +richtigen Rechten im Upload-Ordner, Anmeldung mit dem alten Passwort funktionierte. + +## Zwei Lücken, die erst die Probe gezeigt hat +1. **Die Sicherung ließ sich nur in eine leere Datenbank zurückspielen.** `pg_dump` ohne `--clean` + scheitert an bestehenden Tabellen – genau im Ernstfall, wenn man in eine laufende Installation + zurückspielen will. Jetzt `--clean --if-exists`; gegengeprüft gegen eine Datenbank mit Daten: + null Fehlerzeilen, der zwischenzeitlich geänderte Wert war wieder auf dem Stand der Sicherung. +2. **Für die hochgeladenen PDFs gab es keinen beschriebenen Weg.** Gesichert wurden sie, zurückspielen + musste man selbst herausfinden. + +## Ergebnis +- `deploy/restore.sh`: hält die Dienste an, spielt Datenbank und PDFs zurück, startet wieder und prüft + die Erreichbarkeit. Fragt vorher nach, `--ja` überspringt das. +- Die `.env` wird bewusst **nicht** überschrieben: Sie trägt `APP_SECRET`, und ein falscher Wert dort + macht die gespeicherten Zugangsdaten unlesbar. Die passende Fassung liegt als `env-.txt` daneben. +- `docs/betrieb/sicherung.md` beschreibt den geprobten Ablauf samt Handarbeit als Rückfallebene. + +## Offen +Geprobt wurde der Docker-Weg. Für die Installation als Dienst ist derselbe Ablauf im Skript +vorgesehen, aber noch nicht unter echten Bedingungen gelaufen. diff --git a/docs/issues/README.md b/docs/issues/README.md index a4ea154..3da06e5 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -48,4 +48,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#042](042-qdevice-cluster.md) | Cluster-Knoten falsch gelesen bei QDevice | erledigt | v0.24.1 | 1 h | 0 h 20 min | | [#043](043-laufzeiten-bearbeiten.md) | Laufzeiten anlegen, bearbeiten, zusammenführen | erledigt | v0.25.0 | 4 h | 1 h 05 min | | [#044](044-eigenbedarf.md) | Eigene Hosts ohne Weiterberechnung | erledigt | v0.26.0 | 2 h | 0 h 40 min | -| | **Summe** | | | **175 h** | **24 h 38 min** | +| [#045](045-wiederherstellung-geprobt.md) | Wiederherstellung proben und Skript dafür | erledigt | v0.27.0 | 2 h | 0 h 50 min | +| | **Summe** | | | **177 h** | **25 h 28 min** | diff --git a/docs/roadmap.md b/docs/roadmap.md index f4a8dd4..875f8d3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap -Stand: 04.10.2026, Fassung 0.26.0 +Stand: 10.10.2026, Fassung 0.27.0 Die erste Fassung dieser Roadmap entstand vor dem produktiven Einsatz. Seitdem läuft ProxLedger mit echten Daten, und das hat die Reihenfolge deutlich verändert: Was aus der Benutzung kam, war fast @@ -26,7 +26,7 @@ Kein neuer Funktionsumfang – es fehlt Erprobung. |---|---|---| | **PMG im Feldtest** ([#023](issues/023-feldtest-pbs-pmg.md)) | Der einzige Produktzweig ohne echten Host. Bei PBS lag die Annahme daneben, und der Agent hätte dort nichts gemeldet. | 1 h, sobald ein Host da ist | | **PBS mit aktiver Lizenz** | Bisher nur „keine Subscription“ geprüft. | 30 min | -| **Sicherung einmal zurückspielen** | Eine Sicherung, die nie zurückgespielt wurde, ist keine. | 30 min | +| ~~Sicherung einmal zurückspielen~~ | **Erledigt am 10.10.2026** ([#045](issues/045-wiederherstellung-geprobt.md)) – dabei kamen zwei Lücken heraus. Offen bleibt die Probe für die Installation als Dienst. | – | | **Ein Lizenzjahr durchlaufen lassen** | Warnung → Verlängerung → Einkauf → Kundenrechnung → bezahlt, einmal vollständig in echt. Die nächsten Abläufe stehen am 17.10.2026 an. | nur Zeit | --- diff --git a/package.json b/package.json index 3a55e8e..74b0480 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxledger", - "version": "0.26.1", + "version": "0.27.0", "private": true, "scripts": { "dev": "next dev",