Files
felhom-controller/REPORT.md
T

8.0 KiB
Raw Blame History

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.gotier2CaptureSet (TierSecondary legs + gaps), tier2Leg, tier2DestRel.
  • mod internal/backup/tier2.goselectTier2Target (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.gotier2SSDFits seam.
  • mod internal/backup/appbackup_bridge.goClassMandatory/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 → RestoreTier2Filesrestored (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.