v0.148.0 — coherent snapshot pairs + an offsite restore that actually restores (R-43 + R-44)
Closes the two findings from DIAG-immich-restore-2026-07-19. Viktor deleted 11
immich photos to test offsite restore; both runs flashed success and the photos
stayed gone. Two independent defects.
R-43 — no offsite path could restore a database. All three buttons were
file-only: the two "visszaállítás" actions staged to a scratch folder and never
touched postgres, and place-to-live merged only MISSING files. For a DB-indexed
app the bytes returned and the app still could not see them. The dump was
carried INTO every snapshot and could never be replayed OUT of one.
New ReconstituteFromOffsite (/backup/offbox/reconstitute): safety dump → stop →
files overwritten to the snapshot version → start → the snapshot's own dump
replayed → health wait. Two invariants:
- nothing is ever deleted (-a, no --ignore-existing, no --delete): a file
created after the snapshot survives as an extra;
- the undo exists before the act — the pre-restore- dump is verified ON DISK
before anything is stopped, overwritten or replayed; if it cannot be taken
the operation refuses with zero changes.
The replay reads the SCRATCH unit: the live unit is never overwritten, so
replaying from it would replay the current DB over itself and restore nothing.
R-44 — a manual push shipped an unrefreshed dump (up to ~24h old). That day's
predated the customer's account by four hours and probed to asset:0/user:0/
album:0 inside 52MB whose bulk was immich's shipped geodata. Every run, manual
AND nightly, now refreshes dumps + units BEFORE capturing. Order is the
mechanism: the gap can only ADD files the DB does not reference yet, never
remove one it does. Manifests carry offsite_run_id + dumps_at, so coherence is
verifiable at restore time rather than assumed; the periodic refresh carries a
prior stamp forward and never invents one.
Honesty surfaces, all warn-level and none a gate: unstamped (pre-v0.148) pairs
report their skew, ValidateDump gained an EXACT-match accounts-table sniff for
customer-empty dumps, the completion flash states an outcome instead of a
mechanism, and the missing-only button now says what it does NOT do.
11 tests; 5 red-proofs run and reverted. Two of those found real test weaknesses
rather than confirming strength — the first undo mutation was caught by a second
guard, and the first table-matching test did not discriminate between the two
matchers at all. Both tests were rewritten to the cases that separate them.
NOT in scope: R-41's catalog invariant check, nightly cadence, retention, quota
math, tier-2, and v0.147.x progress semantics beyond one added phase line.
Live acceptance (§9) has NOT run: no capability-map flip, customer-restore row
stays MISSING, R-3 stays DRAFT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9Nn14TWGzKoqAJAiVwC2s
This commit is contained in:
@@ -295,6 +295,26 @@ Each app can define rich metadata in `.felhom.yml`:
|
||||
to a data drive off the rootfs (F-A1) behind a headroom gate; unit-only default via `--include`
|
||||
the absolute unit path; `PlaceOffsiteRestore` merges a full scratch into live via
|
||||
`rsync --ignore-existing` (never `--delete`), refusing on the pure `mapOffsiteRestorePaths` guards.
|
||||
It restores FILES ONLY — no database, no stack restart — and the UI now says so.
|
||||
- **Coherent snapshot pairs (v0.148.0, R-44):** every offsite run — manual AND nightly — refreshes
|
||||
the DB/volume dumps and recovery units (`offsitePreDump` → `runDBDumpsInternal`) BEFORE the
|
||||
restic capture, so each snapshot is an internally coherent `{DB@T, files@T}` bundle and
|
||||
retention is a history of restorable points. Order is the mechanism: the gap can only add files
|
||||
the DB does not reference yet, never remove one it does. Each manifest carries `offsite_run_id`
|
||||
+ `dumps_at`; a manifest without them is a pre-v0.148 pair of unknown skew, surfaced at restore
|
||||
time. The periodic refresh carries the prior stamp forward and never invents one. A dump-leg
|
||||
failure is a loud WARN that does NOT abort the push (data-first: a degraded backup beats none).
|
||||
- **Offsite reconstitution (v0.148.0, R-43 — `offbox_reconstitute.go`):** the leg that was missing.
|
||||
`ReconstituteFromOffsite` (`/backup/offbox/reconstitute`, „Teljes visszaállítás (fájlok +
|
||||
adatbázis)") makes the live app equal to the chosen snapshot: **safety dump → stop → files
|
||||
overwritten (`rsyncRestoreOverwrite`: no `--ignore-existing`, no `--delete`) → start → the
|
||||
snapshot's dump replayed (`reimportDBDumpsFrom`, reading the SCRATCH unit) → health wait**.
|
||||
Two invariants: nothing is ever deleted (post-snapshot files survive as extras), and the
|
||||
`pre-restore-` safety dump is verified on disk BEFORE anything is stopped or overwritten — if it
|
||||
cannot be taken the operation refuses with zero changes. Safety dumps appear in `ListDumpFiles`
|
||||
(they are the undo). The live recovery unit is still never overwritten, which is why the replay
|
||||
source is the scratch. Honesty surfaces (`OffsiteScratchPair`): dump age, an unstamped-pair
|
||||
warning, and the R-44 empty-dump sniff — all warn-level, none of them gates.
|
||||
|
||||
The `/apps/{slug}` page renders hero section, screenshots, setup guide, and optional config form.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user