Offsite tier policy engine: mandatory userdata, raw-data quota, restore rework (Task 3a, v0.134.0)

Each toggled app's offsite push = one multi-path restic snapshot (recovery unit + TierOffsite
mandatory userdata via ComputeCaptureSet); legacy/undeployed stay unit-only. Loud capture gaps
(SP-3.4: restic 0.14.0 silently skips missing paths). Quota = stats --mode raw-data (SP-1;
displayed size drops once). Pre-push enlargement gate blocks the userdata enlargement over-quota
(unit-only push continues; EnlargedBlocked; edge-triggered notify). forget --group-by host,tags
on both sites (SP-2). Restore reworked: scratch off the rootfs + headroom gate (F-A1), unit-only
default via --include, size-first full, place-to-live missing-only merge (never --delete).
UI: unit/full-two-step/place actions + per-app blocked note; route POST /backup/offbox/place.
HUB FLAG: offbox_enlarge_blocked event needs hub allowlist for push delivery.
+13 tests; all 10 §10 red-proofs verified. No tier-2/.fab/hub/agent changes.
This commit is contained in:
2026-07-14 22:51:54 +02:00
parent 0c6e151c1c
commit 2d20859858
17 changed files with 1413 additions and 109 deletions
+50
View File
@@ -1,5 +1,55 @@
## Changelog
### v0.134.0 — Offsite tier policy engine: mandatory userdata, raw-data quota, restore rework (Task 3a) (2026-07-14)
Task 3a of the backup-classification-redesign arc — the FIRST behavior-changing task
(`felhom.eu/documentation/architecture/07-backup-architecture.md` §2/§6/§7/§9; restic mechanisms
proven in `SPIKE-restic-snapshot-shape-2026-07-14.md`). Offsite pushes now carry each app's
**mandatory** userdata, quota is measured as real Storage Box fill, retention survives the shape
change, and restore is reworked off the rootfs. **BEHAVIOR CHANGE.**
- **Multi-path snapshot (§6):** each toggled app's offsite push is now ONE restic snapshot =
recovery unit + the app's TierOffsite mandatory capture set (Task 3-core `ComputeCaptureSet`).
Optional/excluded never ship offsite. Legacy (no block) / undeployed apps stay **unit-only**,
byte-identical to v0.133.0 (the SQ5 cost guard). New `offbox_capture.go`.
- **Loud capture gaps (SP-3.4):** restic 0.14.0 does NOT error on a missing source path (exit 0,
silent partial snapshot), so a structurally-refused or on-disk-missing MANDATORY path is detected
BEFORE invocation (guard `Skipped` list + `os.Stat` filter) and surfaced in the English log **and**
the Hungarian `LastWarning`. A restic exit code never proves a path was captured.
- **Quota = raw-data (§9, SP-1):** `offboxRecordStats` now runs `stats --mode raw-data --json`
(actual deduplicated+compressed repo bytes) instead of the modeless restore-size that multiplied
by the retained-snapshot count. **The displayed remote-backup size drops once after deploy** — it
now reflects the customer's true Storage Box fill.
- **Pre-push enlargement gate (§9, ruling #1):** before an app's enlarged push, if last-known
raw-data repo bytes + the mandatory-set `du` estimate would cross the soft quota, the ENLARGEMENT
is blocked (config+DB unit-only push still proceeds — never a protection regression), the app is
recorded in `OffboxTarget.EnlargedBlocked`, `LastWarning` names it, and an **edge-triggered**
notification fires once per new block (`offbox_enlarge_blocked` event, warning severity). A
per-app "config+DB only" note renders on /backups/remote.
- **Retention grouping (§6, SP-2):** both `forget` call sites gain `--group-by host,tags` so an
app's old unit-only-shape snapshots share a group with its enlarged shape and age out naturally
(the default host,paths grouping would strand old-shape snapshots in a permanently-retained group).
- **Restore rework (§7, F-A1):** new `offbox_restore.go`. Scratch moves off the ~8 GB guest rootfs
to a data drive (`<nsRoot>/backups/offsite-restore/<app>`) behind a headroom gate (full needs
size×1.1, unit-only a 2 GiB floor; ID-first `snapshots latest --tag``stats <ID>`; size-unknown
fails closed). `RestoreOffboxScratch(full)` — unit-only DEFAULT via `--include <absolute-unit-path>`
(SP-3.2), full is a size-first two-step. `PlaceOffsiteRestore` places a completed full scratch into
live locations via a missing-only merge (`rsync -a --ignore-existing`, never `--delete`), unit only
if the live unit is absent; the pure `mapOffsiteRestorePaths` refuses the whole placement on no-unit
/ escape / reserved-zone. Legacy rootfs scratch is cleaned best-effort. The old `RestoreOffbox`
(whole-snapshot to an explicit dest) is retained for existing callers.
- **UI (Hungarian):** /backups/restore offers unit-only ("Visszaállítás ellenőrzéshez"), full
two-step ("Teljes visszaállítás előkészítése" → "…indítása (~méret)"), and place-to-live
("Helyreállítás az élő adatok közé (csak a hiányzó fájlok)"); /backups/remote shows the per-app
quota-blocked note. New route `POST /backup/offbox/place`.
- **Settings:** `OffboxTarget.EnlargedBlocked []string` (replaced each OK run; preserved across a
config edit). **HUB FLAG:** the `offbox_enlarge_blocked` event needs adding to the hub's
`allowedEventTypes` + `customerMessages` for delivery — until then the in-dashboard `LastWarning`
and the /backups/remote note carry the message (see REPORT §flags).
- **Tests:** +13 in `internal/backup/offbox_3a_test.go` (Scenarios AG + all-excluded, raw-data,
both forget sites, restore argv, size-unknown refusal, scratch cleanup, place-to-live mapping);
all 10 §10 red-proofs verified (mutation → fail → revert). No tier-2 / .fab / hub / agent changes.
### v0.133.0 — Capture-set computation (INERT; Task 3-core) (2026-07-14)
Task 3-core of the backup-classification-redesign arc