docs(v0.229.0): R-102 + R-103 — CHANGELOG, CONTEXT rulings, README, REUSE, REPORT
gates / gates (push) Failing after 13s

CHANGELOG v0.229.0. CONTEXT records three rulings: the source moves and the destination does not;
two predicates and not one wider one (R-356's cost restated); and a destructive operation reached
from a non-destructive surface carries the difference in the CONFIRM, not the label. README documents
the new action and route and corrects the coverage note to the measured count. REUSE maps the four
unit-directory-relative primitives and the new manager methods, with the traps.

REPORT covers the live drill on demo-hp (docmost, class B, primary unit moved aside — 3 volumes of 3
and 1 database of 1 in 28.65 s, accented filename byte-identical verified as hex, the app reading its
own row over TCP; Scenario D with the guest app.yaml also aside, secrets recovered=2/2), the settled
count (A=7 B=45 C=1, and why the earlier 9/43/1 was wrong), the five named red-proofs, and seven
observations including R-403 and a process error of mine that changed the box and is now in memory.
This commit is contained in:
2026-08-31 12:21:28 +02:00
parent 4c8f0d2919
commit 8aa95b5831
5 changed files with 413 additions and 406 deletions
+38 -14
View File
@@ -1176,26 +1176,50 @@ operator paths) and per-file selection. Apps that index their data dir (e.g. Nex
rescan (occ files:scan) before restored files appear in their own UI.
> **COVERAGE — read this before assuming an app is protected by this button (C9-F1, v0.183.0).**
> This restore reads `hdd/` and `userdata/` **only**. It has never read `recovery-unit/`, which every
> Tier-2 run also writes and which holds the app's DB dumps and named-volume tarballs. Enumerated
> across all 53 catalog templates: **43 apps have no readable subtree at all** (their data is entirely
> in named volumes — BookStack, Docmost, Vaultwarden, Gitea, …), **9** have file legs but never their
> database or volumes, 1 is stateless. So the button is a guaranteed no-op for 81% of the catalog and
> only ever partial for the rest.
> This restore reads `hdd/` and `userdata/` **only**. It does not read `recovery-unit/`, which every
> Tier-2 run also writes and which holds the app's DB dumps and named-volume tarballs. Counted at
> catalogue `459766cb1639` by running the production rule over all 53 templates (v0.229.0):
> **45 apps have no readable subtree at all** (their data is entirely in named volumes — BookStack,
> Docmost, Vaultwarden, Gitea, …), **7** have file legs but never their database or volumes, and 1
> (bentopdf) is stateless. So this button is a guaranteed no-op for 45 of 53 apps and only ever partial
> for the rest.
>
> Since v0.183.0 it is HONEST about that instead of silently reporting success:
> `Tier2RestoreCoverage` is consulted **before** anything starts, an app with no readable subtree is
> refused **without being stopped** and told which action does work („…Használd a Visszaállítás
> indítása gombot a Biztonsági mentés → Visszaállítás oldalon."), and a run that does proceed claims
> refused **without being stopped** and told which action does work, and a run that does proceed claims
> only what it **examined** („Minden vizsgált fájl megvan a helyén.") plus a disclosure that the
> database and internal volumes are not part of this restore.
>
> The action that DOES cover those apps is the keep-side recovery-unit restore
> (`POST /backup/restore` → `RestoreFromRecoveryUnit`), which replays volume tarballs and DB dumps.
> Routing customers there from the Tier-2 card is filed as **C9-F1b** — it puts a destructive
> operation behind a button reached via a non-destructive one, so the confirm copy must carry that
> difference. **C9-F4** is filed separately: nothing reads the Tier-2 copy's `recovery-unit/` mirror,
> so the second local copy that exists precisely for drive loss is unreachable by any customer action.
> **Since v0.229.0 the action that covers those apps is on the SAME row — see below. C9-F1b / R-103 and
> C9-F4 / R-102 are CLOSED.** The refusal no longer sends anyone to another page: where the copy holds
> an openable unit it names „Teljes visszaállítás a másolatból", the button beside it.
**Full restore FROM THE SECOND DRIVE's mirror (R-102 + R-103, v0.229.0)** —
`POST /backup/tier2/unit-restore` (`backup.RestoreTier2Unit` → `RestoreFromRecoveryUnitAt`,
`internal/backup/tier2_restore.go`) + the **"Teljes visszaállítás a másolatból"** button on the Tier-2
layer row, in `btn-danger-outline` beside the additive one.
Tier-2 mirrors each app's whole recovery unit to `<dest>/backups/secondary/<app>/recovery-unit/` on
every run. Until v0.229.0 **nothing read it**, because every reader of a unit could only name a path
under `backups/primary/` — so in the one failure Tier-2 exists for (the primary drive is lost, and the
primary unit with it) the surviving copy was unreachable. `RestoreFromRecoveryUnitAt` takes the unit
DIRECTORY, so the same restore that always worked from the primary now works from anywhere.
- **The source moves; the destination does not.** Data lands in the live Docker volumes and the live
database container exactly as before; only the read path changes.
- **It OVERWRITES**, unlike the additive button beside it. The two are separate buttons because they
are separate promises, and the confirm carries the difference in words and names the copy's date —
differently when that date is only an ATTEMPT and not a proven copy (R-101).
- **Fail-closed:** the mirror must carry a parseable `manifest.json`. A `recovery-unit/` directory that
exists is not a package, and a restore armed over one would stop the app and replay nothing.
- **Two predicates, not one wider one:** `Tier2Coverage.CanRestoreUnit()` gates this action;
`CanRestore()` still gates only the file restore. Merging them would be R-356 again.
- Refusals (no recorded copy, drive disconnected, pre-v2 layout, no openable unit) all happen **before**
the app is stopped; a second press is refused by `restoreOpBlocked()` (R-351b).
- **Proven live** on `demo-hp` 2026-08-31 with the primary unit moved aside — 3 volumes of 3, 1 database
of 1, 28.65 s, an accented filename byte-identical, and `secrets recovered=2/2` with the guest's
`app.yaml` also moved aside:
`felhom.eu/documentation/audits/DRILL-r102-tier2-unit-2026-08-31/`.
**Per-app Tier-2 config panel (v0.57.0)** — `GET/POST /stacks/{name}/backup`
(`internal/web/tier2_config_handler.go` + `templates/tier2_config.html`). The "2. mentés" row's