Files
felhom-controller/REPORT.md
T

114 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REPORT — Tier-2 engine rework (Task 3b) — controller v0.135.0
## Summary
Task 3b: the tier-2 column of the matrix (architecture §2/§8). The tier-2 appdata leg becomes
**class-driven** for classified apps (per-bind mandatory + optional; paperless's copy shrinks as
`export` drops out), legacy apps keep a byte-identical resolver set — both mapped into a new **v2
relpath-mirroring layout** that lifts the N>1 refusal structurally and gains a reconcile step; the SSD
fallback is an enforced **state-only** tier; and `selectTier2Target` gains the F-6C-1 **network-target
exclusion** (pinned + auto). Restore reads v2 behind a marker gate. Plus **Part 0** (persisted prefs
seed — fixes the 3a-fix un-disableable checkbox) and **Part 0.5** (offsite scratch prefers local).
Every destructive write lands only under `backups/secondary/<stack>/` (code-asserted).
## Baselines (live-verified at session start)
| Repo | `main` @ start | Version | → |
|---|---|---|---|
| felhom-controller | `1245a6c` | v0.134.1 | **v0.135.0** |
## Files
- **new** `internal/backup/tier2_capture.go``tier2CaptureSet` (TierSecondary legs + gaps), `tier2Leg`, `tier2DestRel`.
- **mod** `internal/backup/tier2.go``selectTier2Target` (network exclusion, two sizes, `StateOnly`), `RunTier2` v2 body (migration/mirror/reconcile/marker-last), `tier2SafeRemove` + `tier2Reconcile` + `classifyTier2Rel`, `Tier2Info` rewired, header + `RunTier2` comments truthful; deleted `errTier2MultiDir`/`tier2AppDataName`.
- **mod** `internal/backup/tier2_restore.go` — v2 marker gate + two-subtree missing-only merge; deleted `errTier2MultiDirRestore`.
- **mod** `internal/backup/offbox_restore.go` — Part 0.5 scratch local-preference.
- **mod** `internal/backup/backup.go``tier2SSDFits` seam.
- **mod** `internal/backup/appbackup_bridge.go``ClassMandatory/Optional/Excluded` aliases.
- **mod** `internal/settings/settings.go` — Part 0 seed (`OffboxEnlargeNoticeSeeded` + `seedOffboxEnlargeNotice` at Load; getter append removed); `CrossDriveBackup.LastWarning`.
- **mod** `internal/web/handlers.go` + `templates/backups_apps.html` — surface `Tier2LastWarning`.
- **tests** new `tier2_v2_test.go`, `offbox_scratch_test.go`; rewrote `notif_migration_test.go` (Part 0), `tier2_restore_test.go` fixture (v2), `async_restore_test.go` fixture (v2), `tier2_test.go` signatures; **deleted** obsolete `tier2_appdata_test.go`.
- CHANGELOG / REPORT / CONTEXT / README.
Untouched: offbox engine (except `offboxRestoreScratchDir`), appexport, hub, agent; `rsyncMirror`/`rsyncRestoreMissing` unchanged.
## Tests — results
`go build ./... && go vet ./... && go test ./...`**green.** New: v2 suite (§7 AH + reconcile
keep/remove + safe-remove boundary proof + `classifyTier2Rel` table), Part 0 (idempotent seed +
opt-out-sticks + fresh-customer), Part 0.5 (local-preference + network-only WARN).
### §10 red-proofs (mutate → FAIL → revert), all verified
| ID | Mutation | Test |
|---|---|---|
| A | classified app routed to the legacy resolver | `ClassifiedPaperless` (`export` appears in dest) |
| B | legacy routed through ComputeCaptureSet | `LegacyMultiDir` (empty legs) |
| C | marker written before the legs | `MigrationAndMarkerLast` (marker present after a leg failure) |
| D-reconcile | disable stale detection | `Reconcile` (stale dir survives) |
| D-boundary | neuter the safe-remove prefix assert | `Reconcile` (removal outside destBase not refused) |
| E | SSD copies optional (no state-only filter) | `SSDStateOnly` (optional on the SSD) |
| F1 | drop the pinned IsNetwork check | `NetworkExclusion/pinned` (NAS pin honored) |
| F2 | drop the auto IsNetwork check (NFS iterated first) | `NetworkExclusion/auto` (NAS selected) |
| G2 | neuter the restore marker gate | `RestoreRefusesOldLayout` (merge runs on old layout) |
| Part 0 | don't persist the seed marker | `SeedOffboxEnlargeNotice_OptOutSticks` (type re-enabled) |
All reverted; post-revert full suite green; no residue.
## Deploy / verify
Built + pushed `felhom-controller:0.135.0` on 180; deployed to guest 9201 → `Up (healthy)`. Commit
`3603d1f`.
## §13 live validation
- **Leg 0 (pre-deploy diagnose) — the prompt's expected cause was WRONG.** The nightly
`crossdrive_failed` was **calibre-web** with `LastError = "mkdir …/teszt_enroll/backups/secondary/
calibre-web/recovery-unit…"`. teszt_enroll is a **local** ext4 drive, now mounted + writable — the
failure was a **transient disconnect** at run time, **not F-6C-1**. Confirmed read-only that **no
app auto-targets the `nas-media` NFS path** (all dests are felhom-usb / teszt_enroll). So F-6C-1 is
not the demo's failure cause.
- **Leg 1 — classified v2 tree (calibre-web):** `Tier 2 copied calibre-web → teszt_enroll (258.4 MB,
1 leg, 33s)`; dest tree = `.felhom-tier2-layout`=**2** + `recovery-unit/` + the class-driven
**`userdata/media/books/`** leg (its mandatory bind — not the whole userdata, no flat appdata). Also
the leg-0 before/after: calibre-web was `error` → now `ok`. Migration clean (no old flat `appdata/`).
- **Leg 2 — v2 layout (radarr):** marker=2 + `recovery-unit/` only, **0 legs** (radarr is classified
all-excluded → correctly unit-only). Migration clean.
- **Leg 3 — NAS exclusion:** read-only confirmed no app targets the nas-media NFS path (leg 0);
pinned + auto exclusion is unit-tested (F1/F2 red-proofs) and live in the deployed code.
- **Leg 4 — restore round-trip (calibre-web):** planted a sentinel in live userdata → tier-2 copied it
to the v2 secondary (`userdata/media/books/.felhom-t2probe`) → deleted it live → `RestoreTier2Files`
→ **restored** (the v2 `userdata/` subtree missing-only merge, end-to-end). Sentinel cleaned up.
- **Leg 5 — Part 0 opt-out sticks (LIVE):** unchecked `offbox_enlarge_blocked` + saved + **restarted
the controller** → the checkbox is **still unchecked** and the stored `enabled_events` count is 0 —
the seed did NOT re-enable it (vs the 3a-fix getter-append bug). Pref restored afterward.
- **Leg 6 — customer-email diagnosis:** the controller's `notifications.email` is **EMPTY** (synced as
`email=` to the hub), so the dispatcher has no customer address — the synthetic event's operator-only
delivery is **expected, not a bug** (the customer whitelist has the type, the hub accepts it). Fix =
configure a customer email (operator/customer action); **no hub code change** (flagged per §13-6).
- **Leg 7 — NAS hygiene:** removed the empty F-6C-1-era `nas-media/backups/secondary/` dir (rmdir;
nas-media/backups/ now holds only `primary/` + `offsite-restore/`); the NAS is never a tier-2 target
again.
## 6D-pending (shrunk)
- `PlaceOffsiteRestore` against live data; organic enlarge-block firing; the SQ3 immich offsite-only
full circle (all from Task 3a — unchanged by 3b).
- A pure **legacy (no-block)** app's live tier-2 `hdd/appdata/<name>` leg — not found on the demo (all
toggled apps carry classification blocks); covered by unit tests (`LegacyMultiDir`).
## Observations
- The `Tier2LastWarning` surfacing is minimal (one line on the app's tier-2 row); the SSD state-only
hint was already rendered via `EffectiveIsSSD`/`EffectiveDesc` (no new template needed there).
- `tier2SSDFits` seam added because `system.GetDiskUsage` is Linux-only (nil on the Windows test host)
— production uses the real predicate.
- **Leg-0 finding contradicts the prompt:** the demo's nightly `crossdrive_failed` is a transient
teszt_enroll mkdir failure, not F-6C-1 (no app auto-targets the NFS path). Recorded, not acted on.
- `nas-media/backups/offsite-restore/` (Jul 15) remains — a pre-Part-0.5 offsite scratch on the network
drive (3a-fix era). Out of leg-7 scope (that leg is tier-2 `backups/secondary/` residue); Part 0.5
prevents NEW offsite scratch landing there. Noted for a future offsite-scratch hygiene pass.
- Leg 6: the demo customer has no notification email configured — the customer-email delivery leg is
blocked on that config gap (not code); setting a customer email + re-firing one synthetic event would
complete it.