docs: v0.137.0 cleanup bundle — REPORT + CONTEXT (email-wipe guard + Parts 2-4 disposition)
This commit is contained in:
+15
-1
@@ -7,7 +7,21 @@
|
||||
>
|
||||
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
||||
|
||||
Last updated: 2026-07-15 (v0.136.0 — .fab exclusion scoping, Task 4)
|
||||
Last updated: 2026-07-15 (v0.137.0 — cleanup bundle: email-wipe guard + carried hygiene)
|
||||
|
||||
> **2026-07-15 — v0.137.0: cleanup bundle (email-wipe guard + carried hygiene).** Closes the arc's
|
||||
> carried micro-queue. **Part 1 (code):** `settingsNotificationsHandler` now REFUSES a save with a
|
||||
> blank email box while events are enabled (it would push empty to the hub → wipe the customer's
|
||||
> provisioning-seeded alert address — the 2026-07-15 demo incident). Returns before
|
||||
> SetNotificationPrefs + sync, Hungarian error, repaints submitted checkboxes; empty+zero-events
|
||||
> clear-all still allowed. No HTML `required` (it would block the legit clear-all). Tests + red-proof
|
||||
> in `web/notifications_guard_test.go`. Deployed 0.137.0 to 9201 (healthy). **Part 2 (hygiene):**
|
||||
> removed the confirmed-older `felhom-flash/backups/primary/immich` recovery unit (CreatedAt 06-23 <
|
||||
> live usb 07-15, 44M); STOPPED audiobookshelf/komga/romm on flash (CreatedAt TIED with usb →
|
||||
> tie-break is drive-order-dependent, not confirmable — manual disposition pending). **Part 3:**
|
||||
> campaign6 is a bare empty leftover dir (not a live mount); safe `rmdir` refused (Permission
|
||||
> denied — autofs-ghost/immutable); no mount disturbed → left for Viktor's reboot window. **Part 4:**
|
||||
> tagged the campaign6 6D-audit finding track-only (felhom.eu `dee72cd`).
|
||||
|
||||
> **2026-07-15 — v0.136.0: `.fab` exclusion scoping (Task 4).** Architecture §2 `.fab` row + SQ5
|
||||
> verdict + R1-C. SQ6 over-capture FIXED for classified apps: the userdata root tar is exclude-scoped
|
||||
|
||||
@@ -1,97 +1,69 @@
|
||||
# REPORT — `.fab` exclusion scoping (Task 4) — controller v0.136.0
|
||||
# REPORT — Cleanup bundle: email-wipe guard + carried hygiene — controller v0.137.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.
|
||||
Four closing-the-loop items from the backup-redesign arc + 6D. One code fix (Part 1, shipped
|
||||
v0.137.0), three operational/doc items executed inline.
|
||||
|
||||
## Baselines (live-verified at session start)
|
||||
## Part 1 — empty-email notification save guard (CODE, v0.137.0) — SHIPPED
|
||||
|
||||
| Repo | `main` @ start | Version | → |
|
||||
|---|---|---|---|
|
||||
| felhom-controller | `5f21613` | v0.135.0 | **v0.136.0** |
|
||||
**Problem:** `settingsNotificationsHandler` dropped a trimmed-empty `notification_email` into the
|
||||
prefs and pushed it to the hub (`SyncPreferences`), overwriting the customer's provisioning-seeded
|
||||
alert address — a customer saving any notification change with a blank email field silently wiped
|
||||
their own hub-side alert delivery (the 2026-07-15 demo incident, restored by hand in 6D
|
||||
P3-DELIVERY).
|
||||
|
||||
## Files
|
||||
**Fix:** after computing the trimmed email + enabled events, refuse the save when
|
||||
`email == "" && len(enabledEvents) > 0` — return **before** `SetNotificationPrefs` and any hub sync,
|
||||
re-render with the Hungarian error `"Adj meg egy értesítési e-mail címet – bekapcsolt
|
||||
értesítésekhez szükséges egy cím, ahova küldhetjük őket."`, and repaint the just-submitted
|
||||
checkboxes (render-only overlay on `notificationsPageData`'s `NotificationPrefs`). The legitimate
|
||||
empty-email + **zero** events clear-all still proceeds. `SyncPreferences` / the hub / the
|
||||
seed-migration are untouched. Deliberately **no** HTML `required` attr (it is unconditional and
|
||||
would block the legitimate clear-all — the server-side guard is the precisely-conditional floor).
|
||||
|
||||
- **mod** `internal/appbackup/captureset.go` — extracted `resolveGuardCollapse` (shared pipeline); new `ComputeFabBuckets` + `FabBuckets`.
|
||||
- **new** `internal/appexport/fabplan.go` — `fabPlan`, `computeFabPlan`, `fabUserdataExcludes` + `classifyFabRel`, `tarDirectoryExcluding`, `fabEstimateSplit`.
|
||||
- **mod** `internal/appexport/export.go` — `ExportRequest` += `DeselectOptional`/`OptInExcluded`; `dirLister` seam; `exportHDDData` consumes the plan; `tarDirectory` → thin wrapper.
|
||||
- **mod** `internal/appexport/estimate.go` — `ExportEstimate` class-split fields (additive); `FabItem`.
|
||||
- **mod** `internal/appexport/provider.go` + `cmd/controller/main.go` — `GetStackClassifiedBinds` 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.
|
||||
**Tests + red-proof (`web/notifications_guard_test.go`, real temp-file Settings):** guard-fires
|
||||
(stored email survives the refused save), legitimate clear-all proceeds, normal save persists.
|
||||
Red-proof: neutering the guard → the stored email is wiped to `""` (the incident) → the test fails.
|
||||
Full `go build/vet/test ./...` green.
|
||||
|
||||
Untouched (§5/§12): `manifest.go`, `validate.go`, `restore.go`, every import handler, `ExportDataMounts` — the verdict rests on the import side staying byte-identical.
|
||||
**Deploy + live:** built + pushed `felhom-controller:0.137.0`, deployed to guest 9201 (Up, healthy;
|
||||
`selfupdate: Current version 0.137.0 is up to date`). The protected asset is confirmed present — the
|
||||
demo customer's notification email `nagyfenyvesi.viktor@gmail.com` is set. The authenticated UI
|
||||
click-through needs the customer-owned claim password (CC does not hold it), so the server-side guard
|
||||
is validated by the endpoint-equivalent unit test + red-proof (the CLAUDE.md live-validation proxy).
|
||||
|
||||
## Tests — results
|
||||
## Part 2 — duplicate recovery-unit cleanup (HYGIENE) — 1 removed, 3 STOPPED
|
||||
|
||||
`go build ./... && go vet ./... && go test ./...` — **green.** New: bucket/guard/no-containment
|
||||
(`ComputeFabBuckets`); plan scenarios A–F + §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.
|
||||
Recovery units are `<drive>/backups/primary/<app>/`; the controller's tie-break selects the newest by
|
||||
manifest `CreatedAt`, else dir mtime (`backup/offbox.go:558-607`, first-in-order wins on a CreatedAt
|
||||
tie). On `felhom-flash`:
|
||||
- **Removed** `/mnt/felhom-drives/felhom-flash/backups/primary/immich` (44M) — CreatedAt
|
||||
`2026-06-23T10:02:35Z` vs the live `felhom-usb` copy `2026-07-15T17:41:20Z` → **confirmed older**,
|
||||
never selected. Prefix asserted (`*/felhom-flash/backups/primary/*`) before `rm -rf`.
|
||||
- **STOPPED** (not deleted): `felhom-flash` **audiobookshelf, komga, romm** — their manifest CreatedAt
|
||||
is **identical** to the `felhom-usb` copy (`2026-06-23T10:02:35Z`), so the controller's tie-break is
|
||||
drive-order-dependent and I cannot confirm the flash copy is the non-selected/older one. Per the
|
||||
STOP discipline, left for a manual byte-identity check / disposition (not this micro-task).
|
||||
|
||||
### §10 red-proofs (mutate → FAIL → revert), all verified
|
||||
## Part 3 — campaign6 orphaned autofs (OPERATOR ACTION) — safe clear attempted, left for Viktor
|
||||
|
||||
| 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) |
|
||||
`/mnt/felhom-drives/campaign6` is a bare empty leftover directory (`nobody:nogroup`) — **not** a live
|
||||
mount (not a mountpoint, absent from the mount table, no autofs config / systemd unit / fstab entry).
|
||||
A safe non-disruptive `rmdir` (refuses non-empty dirs + mountpoints) was **refused with Permission
|
||||
denied** even as root (an autofs-ghost / immutable artifact). No live mount was disturbed (mount count
|
||||
89→89; all 8 drive mounts intact). Not escalated to `chattr`/force (beyond a safe clear). **Left for
|
||||
Viktor's demo-host reboot window** — CC did not reboot.
|
||||
|
||||
All reverted; post-revert full suite green; no residue.
|
||||
## Part 4 — 6D audit doc tag (DOC) — committed
|
||||
|
||||
## Deploy / verify
|
||||
Added the matching track-only disposition line to the campaign6 autofs LOW finding in
|
||||
`felhom.eu/documentation/audits/CAMPAIGN-6D-2026-07-15.md` (consistency with the duplicate-recovery
|
||||
LOW). Commit `dee72cd`.
|
||||
|
||||
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.
|
||||
## Queue after this bundle
|
||||
- **Pending operator (Viktor):** campaign6 autofs clears on the next demo-host reboot; the tied-CreatedAt
|
||||
`felhom-flash` audiobookshelf/komga/romm duplicates await a manual disposition.
|
||||
- **Deliberately not here** (correctly deferred): Peti-return runbook (hardware-gated); P4-DEEP timing +
|
||||
P-TIER2 deep-4 (pre-broad-launch resilience); agent 0.88 publish (6D P-DAY0 shows the manifest already
|
||||
vouches 0.88/golden-0.136 — likely closeable, verify separately).
|
||||
|
||||
Reference in New Issue
Block a user