8.0 KiB
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),RunTier2v2 body (migration/mirror/reconcile/marker-last),tier2SafeRemove+tier2Reconcile+classifyTier2Rel,Tier2Inforewired, header +RunTier2comments truthful; deletederrTier2MultiDir/tier2AppDataName. - mod
internal/backup/tier2_restore.go— v2 marker gate + two-subtree missing-only merge; deletederrTier2MultiDirRestore. - mod
internal/backup/offbox_restore.go— Part 0.5 scratch local-preference. - mod
internal/backup/backup.go—tier2SSDFitsseam. - mod
internal/backup/appbackup_bridge.go—ClassMandatory/Optional/Excludedaliases. - mod
internal/settings/settings.go— Part 0 seed (OffboxEnlargeNoticeSeeded+seedOffboxEnlargeNoticeat Load; getter append removed);CrossDriveBackup.LastWarning. - mod
internal/web/handlers.go+templates/backups_apps.html— surfaceTier2LastWarning. - tests new
tier2_v2_test.go,offbox_scratch_test.go; rewrotenotif_migration_test.go(Part 0),tier2_restore_test.gofixture (v2),async_restore_test.gofixture (v2),tier2_test.gosignatures; deleted obsoletetier2_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 A–H + 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_failedwas calibre-web withLastError = "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 thenas-mediaNFS 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-drivenuserdata/media/books/leg (its mandatory bind — not the whole userdata, no flat appdata). Also the leg-0 before/after: calibre-web waserror→ nowok. Migration clean (no old flatappdata/). - 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 v2userdata/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 storedenabled_eventscount 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.emailis EMPTY (synced asemail=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 onlyprimary/+offsite-restore/); the NAS is never a tier-2 target again.
6D-pending (shrunk)
PlaceOffsiteRestoreagainst 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
Tier2LastWarningsurfacing is minimal (one line on the app's tier-2 row); the SSD state-only hint was already rendered viaEffectiveIsSSD/EffectiveDesc(no new template needed there). tier2SSDFitsseam added becausesystem.GetDiskUsageis Linux-only (nil on the Windows test host) — production uses the real predicate.- Leg-0 finding contradicts the prompt: the demo's nightly
crossdrive_failedis 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-2backups/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.