diff --git a/CONTEXT.md b/CONTEXT.md index 300d8f8..8a40e62 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,7 +7,18 @@ > > Ask Claude Code: "Please update CONTEXT.md with what we did today" -Last updated: 2026-07-17 (v0.141.0 — N100 polish: F6 initialize-to-usable + F7 back-routes) +Last updated: 2026-07-17 (v0.142.0 — offsite repo continuity: orphaned-repo guard + run-status auto-refresh) + +> **2026-07-17 — v0.142.0: offsite repo continuity (Parts A + C), LIVE on demo.** Closes the +> reinstall-orphaned-repo incident (a recreated data volume mints a new repo passphrase → the offsite +> repo, keyed under the old one, errors nightly with `wrong password or no key found`). Part A: +> `ensureOffboxRepo` classifies the `cat config` failure → ORPHANED state + calm Hungarian card +> (exception color) + `offbox_repo_orphaned` event (once, not nightly); reset = move-aside (never +> delete, `mv .orphaned-`) + init — UNCLAIMED auto, CLAIMED reveal-then-confirm. +> Part C: `GET /backup/offbox/status` + poll on backups_remote flips Fut→Rendben/Hiba without a manual +> reload. Pairs with hub v0.60.0 (superseded-escrow retention). Live leg staged for the rehearsal +> (scratch-target swap would disturb the live escrow state; live repo untouchable) — mechanism covered +> by 3 fake-based scenarios + 2 red-proofs. Details: REPORT.md. > **2026-07-17 — v0.141.0: N100 polish (F6 + F7), LIVE on demo.** F6 (MEDIUM): drive "initialize" > now ends in a mounted+registered drive even on a client disconnect. `POST /api/storage/init` runs diff --git a/REPORT.md b/REPORT.md index 4d3dd21..48918b6 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,71 +1,48 @@ # REPORT — most recent implementation -## v0.141.0 — N100 polish: initialize-to-usable (F6) + Vissza back-routes (F7) — 2026-07-17 +## v0.142.0 — offsite repo continuity: orphaned-repo guard (A) + run-status auto-refresh (C) — 2026-07-17 -**Baseline:** v0.140.0 (`5be2449`) → **v0.141.0** (deployed live on demo 9201, healthy). Green: -`go build ./... && go vet ./... && go test ./...` all pass; template/emoji/native-confirm gates pass. -Closes `felhom.eu/documentation/tests/VALIDATION-n100-baremetal-2026-07-16.md` F6 + F7. (F5 lives in -felhom.eu `scripts/`; see that repo's REPORT.) +**Baseline:** v0.141.0 (`1452dd2`) → **v0.142.0** (deployed live on demo 9201, healthy). Green: +`go build ./... && go vet ./... && go test ./...` + template/emoji/native-confirm gates. Closes the +reinstall-orphaned-repo incident class (diagnosed 2026-07-17; +`documentation/audits/DIAGNOSE-offbox-repo-orphaned-2026-07-17.md`). Pairs with hub v0.60.0 (Part B). -### Fork verdict (Part 1 Phase 1, source-grounded) -**F6 is controller-side — NO agent change.** The detached mkfs lives agent-side -(`felhom-agent/internal/localapi/formatjob.go:99` + `disks.go:635`, crash-safe, mkfs-only), but the -format→mount→register ORCHESTRATION is `internal/web/storage_handlers.go runStorageInit` and ran on -the REQUEST context; a disconnect cancelled it after `FormatDisk` (agent mkfs continues detached, -returns `errFormatClientGone`) → the mount+register leg never ran → device formatted but unusable. -The chain must reach `SyncFileBrowserMounts` (`handlers.go:2011`, controller-only), so a single -detached job can only live controller-side. The agent already exposes crash-safe format + assign → -agent v0.90.0 was NOT triggered. +### Part A — orphaned-repo guard (Scenarios A/B/C) +- **Classification** (`internal/backup/offbox.go` `classifyResticProbe`): `restic cat config` failure → + `wrong password or no key found` = **ORPHANED** (the definitive 07-17 signature) · no-repo = init · + other (network/SFTP-auth) = unchanged error handling. `ensureOffboxRepo` acts on it. +- **State + card:** `OffboxTarget.RepoState="orphaned"` (+ `OrphanedAt`, `OrphanedRenamedTo`); on the + transition only, `offbox_repo_orphaned` fires and scheduled runs then SKIP (no nightly spam). The + remote page shows a calm Hungarian exception-color card (`backups_remote.html`), NOT the raw restic + banner (`LastError` is cleared for this case); the "Távoli mentés most" button is routed to the card. +- **Reset — move-aside, never delete:** UNCLAIMED box auto-resets on detection (Scenario B); CLAIMED + box gets an explicit reveal-then-confirm reset (Scenario C, `/backup/offbox/reset` + `confirm=1`). + Both do `mv .orphaned-` (collision-suffixed `-2/-3`, over an ssh-exec seam) + + `restic init` + `offbox_repo_reset`. Restore paths never auto-reset (the auto-reset is run-only). +- **Red-proofs (run-fail-revert):** `TestOffbox_OrphanDetection_Claimed` (pre-fix = the incident: the + raw `wrong password` error surfaces, no state → FAIL; fixed: clean skip, one event, no nightly + re-fire, no raw banner) + `TestOffbox_OrphanDetection_UnclaimedAutoReset` (move-aside + both events, + un-orphaned) + `TestOffbox_ConfirmedReset` + `TestClassifyResticProbe`. -### F6 — initialize now ends in a mounted+registered drive, disconnect-safe -- `POST /api/storage/init` starts a DETACHED single-flight job (`internal/web/storage_init_job.go`, - the `netAddState` shape) on `context.Background()`; `runStorageInit` gained a nil-safe phase callback - (formatting → mounting → registering). The wizard polls `GET /api/storage/init/status` and renders - the 3-step progress (`storage_init.html`); confirm/refuse verdicts surface through the same poll. - Register is the LAST step (marker-last, Scenario B) + idempotent prior steps (`AddStoragePath` - dedups) → a crash leaves at most an unregistered orphan, never a broken/duplicate registration. -- **Deeper half (found on the live leg):** a slow mkfs (a 64 GB USB) outruns the agentapi client's - 15 s `Timeout`; the agent runs it DETACHED and records the job, so `runStorageInit` now POLLS the - agent's `GET /disks/format/status` (new `agentapi.Client.FormatStatus` → `awaitAgentFormat`) to the - terminal outcome on a client timeout, then continues to mount+register (the F6 root-cause's "mkfs - continues detached; poll the status"). -- **Red-proofs (run-fail-revert):** `TestStorageInit_DetachedSurvivesClientDisconnect` (pre-fix - cancelled-ctx chain fails at mount, NOT registered → FAIL; fixed: detached job registers exactly - once) + `TestStorageInit_PollsAgentFormatStatusOnTimeout` (timeout→done registers; timeout→failed - surfaces the error). +### Part C — run-status auto-refresh (Scenario E) +`GET /backup/offbox/status` (JSON: status/snapshots/last_error/orphaned) + a poll on +`backups_remote.html`: while a run shows "Fut…" the page polls every 3 s and reloads once at the +terminal state (Rendben/Hiba + fresh numbers) — no manual reload; polling stops at terminal. Does not +touch the scheduled-run rendering. Test `TestOffboxStatusHandler` + `TestOffboxRun_RefusedWhenOrphaned`. -### F7 — Vissza back-routes -The "Vissza" (Back) anchor on `/storage/init` and `/storage/attach` now routes to `/storage` (was -`/settings`); the init success link also points to `/storage`. Test -`TestStorageWizardBackAnchors_PointToStorage`. +### Live leg (§13) — fallback taken, staged for the rehearsal +The scratch-target live leg CANNOT run without disturbing live state: configuring a scratch offsite +target resets `EscrowState` to `pending` (the offsite config carries the escrow state + the escrowed +password), the offbox RUN gates on `EscrowState=="escrowed"` (a scratch target needs its own +customer-driven ceremony), and the live repo (customer zero's first snapshot) is untouchable (rule 2). +Per §13's explicit fallback: the mechanism is covered by the fake-based tests above (the classification +uses the EXACT restic stderr matched in the real 07-17 diagnosis), and the live leg is STAGED for +Viktor's planned delete/re-create rehearsal. Deploy verified: controller 0.142.0 live+healthy on 9201; +the demo's healthy live repo was NOT spuriously flagged (settings `repo_state` absent = normal). -### Live validation (F6 Scenario A, on the demo scratch USB) -Method: the REAL controller endpoints (curl against the in-guest controller with the dashboard Host -header + a login session + the CSRF token — the exact calls the UI makes; browser tool not used). -Scratch device `/dev/sdd` (serial `20120926571200000`, 59.5 GB) serial-checked against the enrolled -list FIRST (≠ the enrolled `/dev/sdb` hdd_1; ≠ OS `/dev/sda`) and confirmed present in the -agent-proven `initialize` candidates. Flow: init (no confirm) → `needs_confirmation` (ext4) via poll → -init (confirmed) → `{started}` (returns immediately; the client can leave) → detached job -`formatting` (~27 s mkfs, past the 15 s client timeout → the status-poll fix engaged) → `mounting` → -**`done`** at `/mnt/felhom-drives/scratch1`. Verified: `/dev/sdd` mounted at the enrolled stable bind -`/mnt/felhom-drives/scratch1` (`/dev/sdd[/felhom-data]`); the enrolled hdd_1 untouched. - -### Security-review acknowledgment (background review of the commit) -Flagged a TOCTOU in `runStorageInit` (format → `resolveEnrollUUID(path)` → `AssignDisk(uuid)`). -**Assessment:** pre-existing (unchanged sequence — F6 only moved it off the request context and added -the timeout status-poll, which widened the window by the poll interval). The DESTRUCTIVE step (format) -is the authorization boundary and is agent-enforced with anti-retarget **durable-id** binding — the -controller cannot make the agent format the wrong device; the subsequent mount is benign and keyed by -**fs-UUID** (a moved /dev node resolves to a non-existent UUID → a mount failure, not data loss or an -authz bypass). No new authorization bypass. Left as an Observation (not expanded this batch per the -minimal-changes mandate); a future hardening could bind resolve+assign to the format's durable-id. - -### Operator sidebar (Viktor — CC does NOT do these; recorded verbatim from the spec) -1. **BIOS "State After G3" → "Power On"/"Last State"** before final sign-off — also added to the - hardware-prep notes in `scripts/iso/README.md`. -2. **campaign6 autofs orphan** — clears at the next host reboot. -3. **tied-CreatedAt flash duplicates** (audiobookshelf/komga/romm) — standing action item. - -### NOT live this task -F5 (guest auto-sizing) is validated by harness only; live at the next from-scratch rehearsal -(Viktor's planned delete/re-create run) — see felhom.eu REPORT. +### Observations +- Orphaned bytes vs quota: counted honestly in the usage bar; the card notes the set-aside copy; a + purge affordance is NOT built (Viktor purges the `.orphaned-` dir manually — as in the 07-17 fix). +- Other backup pages' refresh behavior unchanged (Part C is scoped to the remote-run status only). +- The controller cannot know the hub's superseded-blob count without cross-repo plumbing, so the card's + "recoverable with the recovery code" phrasing is unconditional (true going forward — Part B retains).