docs: v0.99.0 — CHANGELOG/CONTEXT/REUSE/README for the restore-path fixes (F1/F3/O4)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-05 11:56:48 +02:00
parent a52851e79e
commit 88362dac0a
4 changed files with 82 additions and 14 deletions
+32 -11
View File
@@ -441,8 +441,14 @@ backups/primary/<app>/
Docker image** — only the pinned image tag(s) (re-pulled on restore) and the *names* of the secret /
`data_key` env vars. Rationale: app.yaml + the encryption key live on the guest rootfs → already in
the PBS whole-guest snapshot, and the hub is deliberately zero-knowledge. Restore recovers the
original secrets from the guest's own app.yaml (live, or via PBS) and **regenerates nothing**; for a
`data_key` app it **fails closed** (refuse + warn) if the key can't be recovered.
original secrets from the guest's own app.yaml (live, or via PBS); for a `data_key` app it
**fails closed** (refuse + warn) if the key can't be recovered — data-keys are NEVER generated.
**Resettable secrets (O4, v0.99.0):** an unrecoverable resettable secret (DB password etc.) gets a
**generated replacement** from its catalog `generate` spec (`stacks.GenerateSecretForField` via the
`backup.SetSecretGenerator` seam) instead of redeploying blank (which failed compose-up); the new
value persists encrypted through the normal `RecreateStackFromUnit``SaveAppConfig` path. Fields
with no `generate` spec still proceed with a loud "may fail to start" WARN. Residual case: a restored
volume tar carrying the OLD internal credential hash may still need a manual in-DB reset.
- Helpers: `RecoveryUnitPath` / `RecoveryUnitComposePath` / `RecoveryUnitManifestPath`
(`internal/appbackup/paths.go`). Capture: `Manager.CaptureRecoveryUnit` (`internal/backup/recovery_unit.go`),
run from the daily DB dump and the periodic `RefreshCache` (idempotent checksum-skip). The non-secret
@@ -492,15 +498,23 @@ re-validates the pin off-disk at run time. `Manager.Tier2Info(stackName)` is the
- **Validation** after each dump: checks file size, header presence, counts `CREATE TABLE`
- Results cached in `settings.json` surviving container restarts
**Phase 1b — Docker Volume Dumps** (`internal/backup/backup.go`, runs after DB dumps)
**Phase 1b — Docker Volume Dumps** (`internal/backup/backup.go` `runVolumeDumps`, part of the same run)
- Iterates all deployed stacks that have Docker named volumes (`GetDockerVolumes()`)
- **v0.34.0:** Each stack is stopped before dump, restarted after (`DumpAppVolumesSafe()`) — prevents inconsistent tars of live databases. Protected stacks (traefik, etc.) that reject StopStack are skipped with a warning.
- **F3 (v0.99.0):** re-wired into the nightly/manual app-data backup run (`runDBDumpsInternal`)
after the restic removal `DumpAppVolumesSafe` had no caller, so `volume-dumps/` was never produced.
Runs AFTER the DB dumps and BEFORE `captureAllRecoveryUnits` so the manifests enumerate fresh tars.
- Gate order (load-bearing): protected-stack (`cfg.IsProtectedStack`) and has-volumes
(`GetDockerVolumes()`) checks come BEFORE `DumpAppVolumesSafe` — the Safe variant stops the stack
before its own volume check, so unconditional calls would bounce every volume-less app nightly.
Disconnected/decommissioned drives skip with the same summary style as the DB loop.
- Each volume-bearing stack is stopped before dump, restarted after (`DumpAppVolumesSafe()`) —
prevents inconsistent tars of live databases.
- For each volume: `docker run --rm -v <vol>:/vol:ro -v <dumpDir>:/out alpine tar cf /out/<vol>.tar -C /vol .`
- 10-minute timeout per volume; warnings on failure (non-fatal)
- 10-minute timeout per volume; a per-stack failure lands in the run summary as `FAIL <app> volumes:`,
flips the run's Success flag and fails the run (no silent partial) — other stacks still proceed
- Stale tars cleaned up (volumes that no longer exist)
- Volume names resolved with project prefix via `ResolveDockerVolumeNames()` (e.g., `mealie_mealie_data`)
- Dumps written to `AppVolumeDumpPath(appDrive, stackName)`
- Dumps written to `AppVolumeDumpPath(nsRoot, stackName)`
**Phase 2 — Restic Snapshot** (`internal/backup/restic.go`, scheduled 03:00)
@@ -589,10 +603,17 @@ appear in the restore dropdown with per-app snapshot filtering.
| DB only, no HDD/volumes | Yes | Yes | n/a | n/a |
| Config only | Yes | — | n/a | n/a |
**Snapshot API** (`/api/backup/snapshots?stack=<name>`):
- Returns snapshots **only from the app's home drive** primary repo (prevents showing irrelevant snapshots from other drives)
- Appends a synthetic Tier 2 entry (ID `tier2-rsync`) from cross-drive config when last backup was successful
- Dropdown groups by tier: "1. szint — Helyi mentes" and "2. szint — Masodlagos masolat"
**Snapshot API** (`GET /api/backup/snapshots?stack=<name>` — F1, v0.99.0):
- Backed by `backup.Manager.ListRestorePoints` (`internal/backup/restore_points.go`). The keep-side
restore has exactly **one** restore point per app — the current recovery unit — so the endpoint
returns at most one entry: `time` = newest artifact mtime (manifest / db-dumps / volume-dumps),
`short_id:"helyi"`, `tier:1`, `drive_label` from the storage registry (empty on the SSD fallback)
- **Never emits tier-2 entries**: Tier-2 copies are not restorable via `POST /backup/restore` (it
only reads the primary unit) — listing them would silently restore tier-1 data while claiming tier-2
- Guards: empty/traversal stack name → 400 (`validStackParam`), unknown stack → 404, known stack
with no unit yet → `ok:true, data:[]` (the UI shows "Nincs elérhető mentés")
- History: the route was a restic-era leftover fetched by the template but unregistered — the
dropdown could never populate and the restore button never enabled (drill finding F1)
**Restore type info** shown per-app when selected in dropdown (Hungarian banners):
- Has HDD or Docker volumes: "Teljes visszaallitas: adatbazis + konfiguracio + felhasznaloi adatok"