Files
felhom-controller/REPORT.md
T

6.3 KiB
Raw Blame History

REPORT — .fab exclusion scoping (Task 4) — controller v0.136.0

Summary

Task 4: the .fab column of the matrix (SQ5 verdict + ruling #1 + Viktor's R1-C). The SQ6 over-capture is fixed for classified apps — a manual export's userdata root tar is now exclude-scoped (keeps only ancestors/descendants of a selected bind), so sibling apps' content no longer rides along; an all-excluded app produces no userdata tar at all. Mechanics unchanged from v0.130.0 (one root tar + per-mount skip), so the manifest stays v1 and the import side is untouched; legacy apps export byte-identically. Mandatory is a server-side floor; optional is a pre-selected checkbox; excluded is opt-in behind the two-number warning.

Baselines (live-verified at session start)

Repo main @ start Version
felhom-controller 5f21613 v0.135.0 v0.136.0

Files

  • mod internal/appbackup/captureset.go — extracted resolveGuardCollapse (shared pipeline); new ComputeFabBuckets + FabBuckets.
  • new internal/appexport/fabplan.gofabPlan, computeFabPlan, fabUserdataExcludes + classifyFabRel, tarDirectoryExcluding, fabEstimateSplit.
  • mod internal/appexport/export.goExportRequest += DeselectOptional/OptInExcluded; dirLister seam; exportHDDData consumes the plan; tarDirectory → thin wrapper.
  • mod internal/appexport/estimate.goExportEstimate class-split fields (additive); FabItem.
  • mod internal/appexport/provider.go + cmd/controller/main.goGetStackClassifiedBinds on the export seam + adapter.
  • mod internal/web/handler_export.go + handler_export_download.go — both start handlers carry the selections (two-call-site).
  • mod internal/web/templates/app_export.html — the class-selection UI.
  • tests new fabbuckets_test.go, fabplan_test.go, fabexport_test.go, fab_export_test.go; provider fakes updated.
  • CHANGELOG / REPORT / CONTEXT / README.

Untouched (§5/§12): manifest.go, validate.go, restore.go, every import handler, ExportDataMounts — the verdict rests on the import side staying byte-identical.

Tests — results

go build ./... && go vet ./... && go test ./...green. New: bucket/guard/no-containment (ComputeFabBuckets); plan scenarios AF + §8 (unmatched-mount-kept, skip-when-none, mandatory floor, nested excludes); tarDirectoryExcluding FS-level; export-level bundle tests (exclude-scoped tar, legacy full-root, all-excluded no-tar); the two-call-site bundle test across BOTH start pipelines.

§10 red-proofs (mutate → FAIL → revert), all verified

ID Mutation Test
A legacy routed through the classified plan (drop short-circuit) FabExport_LegacyFullRoot (userdata tar skipped)
B always-tar the userdata root FabExport_AllExcludedNoUserdataTar (tar present)
C invert the keep-rule FabExport_ExcludeScopedUserdataTar (mandatory excluded)
D3 drop the server-side mandatory floor FabPlan_MandatoryFloor (mandatory deselected)
E skip guards for excluded buckets ComputeFabBuckets_GuardsAllClasses (traversal enters a bucket)
F drop selection fields from one start handler Fab_SelectionsRideBothStartPipelines/download (opted-in content absent)

All reverted; post-revert full suite green; no residue.

Deploy / verify

Built + pushed felhom-controller:0.136.0 on 180; deployed to guest 9201 → Up (healthy). Commit cf9ce01.

§13 live validation — the SQ6 before/after, on the real demo

  • Leg 1 — before-picture (v0.135.0, the SQ6 protocol): planted SPIKE-marker-own.txt under calibre-web's media/books/ and SPIKE-marker-sibling.txt under the sibling media/movies/; exported calibre-web via the real UI (download pipeline, 356 MB). tar tf of the bundle's data/hdd/userdata.tar: BOTH markers present (own=1, sibling=1, 15 media/movies entries) — the whole userdata root rode along (the over-capture).
  • Leg 2 — after (v0.136.0): re-exported calibre-web (default). tar tf: own=1, sibling=0, media/movies entries=0, media/books entries=39 — the sibling no longer rides along (Scenario C live). radarr default export → the bundle has no hdd/ contents at all (no userdata tar, Scenario B live — radarr is classified all-excluded).
  • Leg 3 — opt-in + estimate split: radarr's download/estimate returns has_classification:true + excluded_items:[{downloads, 308 MB}, {media/movies, 2.7 GB}] (the two-number-warning data, live, both pipelines). Re-exported radarr with opt_in_excluded:["userdata/downloads"] → the userdata tar now carries downloads (2 entries) while media/movies stays excluded (0) — the opt-in flips Scenario B, per-path, live.
  • Leg 4 — import compatibility (read-only): the new exclude-scoped bundle's manifest is v1 (version=1, hdd_subdirs=['userdata'], has_hdd_data=true) — the untouched import maps the userdata basename → <HDD_PATH>/userdata via the existing fallback, so the smaller tar restores exactly as before. A destructive live import over calibre-web (compose down --volumes) was NOT run — compatibility is structurally guaranteed (v1 manifest + untouched import) and unit-proven (roundtrip_test.go).
  • Leg 5 — hygiene: removed the two SPIKE markers from live userdata and the test .fab bundles; cleaned the 180 session temp. Listed removals above.

6D-pending

  • CAMPAIGN-6D Accept #1 (the ≥1 GiB .fab full circle) now runs against this final capture shape — a real export→transport→import round-trip of a large classified app, exercising the exclude-scoped userdata tar end-to-end (the live leg-4 import was deliberately read-only here).
  • Carried from earlier tasks: PlaceOffsiteRestore against live data; the SQ3 immich offsite-only full circle; organic enlarge-block firing.

Observations

  • ComputeCaptureSet was refactored to share resolveGuardCollapse with ComputeFabBuckets (no duplicate pipeline); its existing tests stayed green through the refactor.
  • The estimate split is in the shared EstimateExport, so both estimate pipelines surface it by construction; the genuine two-call-site risk (and the F red-proof) is on the START selections, which the bundle test exercises per-handler.