docs(audit): backup-classification spike — coupling model VALIDATED (Immich DB-only restore broken-not-empty); offsite=unit-only live-proven; NEW F-S1 (tier-2 copies no userdata) + F-S2 (paperless-ngx appdata skipped); round-trip verdict: exclusion scoping

This commit is contained in:
2026-07-14 16:39:55 +02:00
parent 671bd3fd91
commit 790ec845f7
2 changed files with 447 additions and 33 deletions
+42 -33
View File
@@ -2,42 +2,51 @@
> **Overwrite** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md](hub/CHANGELOG.md); the scripts history lives in [scripts/CHANGELOG.md](scripts/CHANGELOG.md).
## CAMPAIGN-6B — unattended close-out of the C6 remainder — 2026-07-14
## SPIKE — referential-coupling backup classification — 2026-07-14
**controller 0.129.0 / agent 0.88.0 / hub 0.54.0 · demo + drill · findings-only, unattended**
**controller 0.130.0 (demo 9201) / agent 0.88.0 · findings-only, no production code · full audit:
`documentation/audits/SPIKE-backup-classification-2026-07-14.md`**
Continuation of CAMPAIGN-6A. Drove the `.fab` full-circle (upload/download + real Cloudflare edge),
the server-observable browser/DOM planes, backup-tier + NAS integrity, and regression spot-checks.
Full audit: `documentation/audits/CAMPAIGN-6B-2026-07-14.md`. Continuation: `CAMPAIGN-6C-2026-07-14-PROMPT.md`.
Empirical ground-truth pass for the userdata-backup-classification redesign
(mandatory=COUPLED / optional=DECOUPLED-precious / excluded=DECOUPLED-bulk).
### Headline finding — C6B-F1 (CRITICAL)
**`.fab` export silently produces hollow, data-free bundles for the standard `${USERDATA_PATH}`
`needs_hdd` app convention** — 12 of 13 media catalog apps (jellyfin, plex, emby, immich, sonarr,
radarr, navidrome, paperless-ngx, audiobookshelf, calibre-web, komga, romm). Live: sonarr with
**4.17 GB / 7 files** exported to a **2308-byte, config-only** bundle, reported `done` with no error,
past the v0.125.0 anti-hollow guard. Two compounding causes in the `needs_hdd` branch: `executeExport`
is either/or (`needs_hdd` apps never run `exportVolumeData`, dropping named volumes incl. the app DB),
and `ParseComposeHDDMounts` substitutes only `${HDD_PATH}`, never `${USERDATA_PATH}` → 0 mounts → all
userdata skipped. The guard only checks manifest-*claimed* tars, so 0-claimed passes. `.fab` is the
customer portability/backup-download path → fresh/cross-box restore = silent total data loss. Fix
direction in the audit; **priority for project Claude to spec.**
### What was established
### What passed
- **`.fab` transport is byte-clean end-to-end** — LAN + real Cloudflare edge, upload (64 MiB chunked)
and download, every sha256 == reference.
- **413 edge-cap re-proven** (120 MiB → CF 413 before origin; 80 MiB clears to origin).
- **Upload state machine 8/8** — wrong-ext 400, collision `(N)`, single-flight 409, strict-offset 409,
abort cleanup, oversize (both Hungarian numbers), restart-GC, idle-15min expiry.
- **F7 atomicity** (`.tar.tmp`→rename) code-confirmed; **native `alert()`/`confirm()` absent** on all
surfaces; CSRF stale → 403, session-expiry → 401-JSON/redirect; **F4** uid-range friendly 400;
agent-restart per-share re-arm verdicts logged.
- **SQ1 — offsite carries no userdata today, no exception (source + live).** All 12 restic
snapshots are `backups/primary/<app>` units only; a full `restic ls` of the newest immich
snapshot shows compose + `immich-postgres.sql` + manifest — **immich is offbox-toggled and its
photo library is in none of its snapshots** (the latent broken-restore gap, now live-proven).
- **NEW F-S1:** tier-2 does **not** copy bulk userdata — `RunTier2` rsyncs only the unit +
`appdata/<stack>`; the `tier2.go:17` "recovery unit + bulk userdata" header and the
`main.go:1360` "namespace wholesale" comment are stale. Browsable HDD userdata has **zero
automated off-drive protection today** (live: `secondary/{radarr,jellyfin}` hold only
`recovery-unit/`).
- **NEW F-S2:** tier-2's appdata leg keys on the stack name → **paperless-ngx**
(`appdata/paperless`) is silently skipped (live contrast: `secondary/nextcloud` has `appdata/`,
`secondary/paperless-ngx` doesn't) — its document originals get no tier-2 copy.
- **SQ3 — the coupling model is VALIDATED (scratch Immich, catalog images v2.5.5).** DB-present /
library-absent restore: the server **refuses to boot** (its own `.immich` folder-check, with the
mount state remembered *in the DB*); with the skeleton recreated it boots into a **lying
gallery** — timeline reports all 3 assets, every thumbnail/original fetch is HTTP 500. No repair/
re-import path. Restoring the files → all 200 again. **Worse-than-empty: confirmed.**
- **SQ2** — full per-bind classification for all 55 catalog apps (13 hdd + 42 volume-only);
shared-media-tree consistency resolved by the "class belongs to the bind; the writing app is the
authority; `:ro` readers back up nothing" rule.
- **SQ4** — size split measured live (state 0.5 M933 M vs bulk tree ~100 MB demo /
TB-scale prod); the two-number estimate reuses the existing `estimate.go` per-mount du +
container-view volume sizer — zero new measurement plumbing.
- **SQ5 — round-trip verdict: EXCLUSION scoping** (tar the userdata root minus excluded subtrees).
Per-bind nested tars cannot ride the v1 manifest (`HDDSubdirs` basenames; CTRL-001 forbids path
separators); exclusion scoping needs no manifest/import change and stays backward-compatible.
Proposed `backup:` block schema + **two-level default** (no block → legacy per-tier behavior;
block present → unlisted writable binds mandatory) — the task's single-level mandatory-default
premise would have shipped unmigrated media trees offsite (quota blow-up) and was corrected.
- **SQ6** — `.fab` over-capture quantified on the live tree (radarr bundle carries ~1.6× its own
data even on the small demo tree, unbounded in prod); the live marker-file export leg is blocked
on the customer-owned dashboard login (same blocker as the open Viktor full-circle test).
### Split to CAMPAIGN-6C
Supervised browser planes (escrow wizard click-through, hub 8-tab UX); deep backup tiers (live F7
6 s-cut, restic self-heal, offsite restore-to-verify, tier-2 F6, per-app toggles, snapshot coherence);
dead-app alert + email cooldown; the **C6B-F1 fix-verify** full-circle byte-compare.
### Cleanup
### Notes
- No code changed. A temporary break-glass credential was used on the demo controller to drive the
authed tests, then **fully reverted at cleanup** (original customer-claimed hash restored) — the box
is back to its pre-6B credential; **Viktor rotates** that. No secret/R/blob committed.
Scratch Immich fully removed (containers/volumes/network/files/images; verified 0 spike containers,
controller healthy). Restic inspection read-only (`--no-lock`), 12 snapshots before and after. No
drive namespace, tier-2 target, or deployed app touched.