feat(escrow): v0.138.0 — "awaiting hub confirmation" waiting state
After a completed escrow ceremony the Távoli mentés page showed the yellow "Helyreállítási kód szükséges" card for ~15 min until the next hub-report ACK flipped pending→escrowed. Phase-0 diagnosis (read-only) = verdict A (report-cycle lag), already resolved on the demo box (escrow_state:"escrowed"); hub Hypothesis B verified false (SaveHostEscrow ON CONFLICT already clears stale_at on upload) → no hub change. - settings.OffboxTarget.CeremonyCompletedAt: stamped on the recovery-code claim, zeroed on the auto-confirmer Flip + the deprecated manual confirm; persisted. - web/handlers.go: offboxCeremonyWaitState + escrowCeremonyGraceWindow (35m). - backups_remote.html: info "megerősítésre vár, legfeljebb 15 perc" card → warn "a megerősítés nem érkezett meg" past the window. Existing branches untouched. - backups_escrow.html: "Mi történik ezután?" note on the wizard's final step. - Test web/escrow_wait_state_test.go (truth table + red-proof recorded in REPORT). No scheduler/agent/hub/endpoint changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qDiBqKKQ5vPB5fXBqu7Kp
This commit is contained in:
@@ -1,69 +1,102 @@
|
||||
# REPORT — Cleanup bundle: email-wipe guard + carried hygiene — controller v0.137.0
|
||||
# REPORT — felhom-controller v0.138.0: escrow "awaiting hub confirmation" waiting state
|
||||
|
||||
## Summary
|
||||
**Task:** Diagnose the stuck "Helyreállítási kód szükséges" banner after a completed escrow ceremony
|
||||
on the demo box; add the post-ceremony waiting-state UX; ship the hub stale-clear fix **only if**
|
||||
Phase-0 diagnosed Hypothesis B.
|
||||
|
||||
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.
|
||||
**Shipped:** controller **v0.138.0** (Part 1, unconditional). **Hub: NO change** (Phase-0 = A, and
|
||||
Hypothesis B verified false). felhom-agent untouched.
|
||||
|
||||
## Part 1 — empty-email notification save guard (CODE, v0.137.0) — SHIPPED
|
||||
---
|
||||
|
||||
**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).
|
||||
## Phase 0 — DIAGNOSIS (read-only). Verdict: **A (report-cycle lag)** — already resolved on the box.
|
||||
|
||||
**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).
|
||||
The banner renders while `Offbox.EscrowState != "escrowed"`. The pending→escrowed flip happens only
|
||||
in `report.EscrowAutoConfirmer.Reconcile` on a report ACK, on hash-match between the ACK's
|
||||
`restic_pw_sha256` and the local repo password. The observed stuck banner was the ~15-min gap between
|
||||
a completed ceremony and the next ACK.
|
||||
|
||||
**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.
|
||||
**Evidence — demo box `felhom-controller` logs (guest 9201, running 0.136.0):**
|
||||
```
|
||||
2026/07/16 16:13:31 [INFO] [web] escrow ceremony started via wizard (job escrow-1784218411576550074)
|
||||
2026/07/16 16:13:39 [INFO] [web] escrow recovery code claimed (one-shot; not logged)
|
||||
2026/07/16 16:27:58 [INFO] [report] Hub report pushed successfully (2992 bytes)
|
||||
2026/07/16 16:27:58 [INFO] [escrow-confirm] hub-verified: the escrow covers the current repo password (hash d517ce7f9982…) — EscrowState auto-confirmed escrowed; offsite runs enabled
|
||||
```
|
||||
- Ceremony claimed `16:13:39`; the **very next** `hub-report` ACK at `16:27:58` (~14 min later)
|
||||
flipped pending→escrowed via hash-match (`d517ce7f9982…`). One INFO confirm line, no mismatch
|
||||
warning, no withheld-hash gap.
|
||||
- **Current box state:** `settings.json` → `"escrow_state": "escrowed"` (verdict A resolved itself;
|
||||
the box is confirmed; `OffboxRunnable()` true).
|
||||
|
||||
**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).
|
||||
**Hypothesis B (hub never clears stale on upload) — verified FALSE, so NO hub change:**
|
||||
- `store.SaveHostEscrow` (the escrow **upload** sink, `handler.go:1083`) already clears stale on a new
|
||||
blob: `INSERT … ON CONFLICT(host_id) DO UPDATE SET … stale_at = NULL` (`store.go:2084`).
|
||||
- The ACK only withholds the hash **while `stale_at` is set** (`store.go:2150`: `if staleAt != ""`).
|
||||
- So a re-issue→re-ceremony cycle recovers on its own; the only thing not emitted is a cosmetic
|
||||
`escrow_refreshed` event — out of scope (Part 2 gated on B, which does not hold).
|
||||
|
||||
## Part 2 — duplicate recovery-unit cleanup (HYGIENE) — 1 removed, 3 STOPPED
|
||||
**Hypothesis C (upload silently failed):** ruled out — the box reached escrowed via a hash-bearing
|
||||
ACK, which requires the fresh blob + hash to be present on the hub.
|
||||
|
||||
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).
|
||||
**Operational note:** the demo Proxmox host key changed since the last session (box reprovisioned for
|
||||
the N100 customer-zero run). Refreshed `known_hosts` (`accept-new`) before any read — expected for the
|
||||
demo host, not a security event.
|
||||
|
||||
## Part 3 — campaign6 orphaned autofs (OPERATOR ACTION) — safe clear attempted, left for Viktor
|
||||
---
|
||||
|
||||
`/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.
|
||||
## Part 1 — controller v0.138.0 (the waiting state)
|
||||
|
||||
## Part 4 — 6D audit doc tag (DOC) — committed
|
||||
1. **Persisted stamp.** New `settings.OffboxTarget.CeremonyCompletedAt` (`ceremony_completed_at`,
|
||||
RFC3339). SET on a successful recovery-code **claim** (`web/escrow_handlers.go`, only while still
|
||||
pending; best-effort — a stamp failure never fails the claim, the code is already revealed and the
|
||||
blob already uploaded). ZEROED on the pending→escrowed flip — the auto-confirmer `Flip` closure
|
||||
(`cmd/controller/main.go`) **and** the deprecated manual confirm (`web/offbox_handlers.go`).
|
||||
Persisted ⇒ survives a controller restart mid-wait.
|
||||
2. **The pick.** `web/handlers.go`: `offboxCeremonyWaitState(t)` → (awaiting, timedOut);
|
||||
`escrowCeremonyGraceWindow = 35m` (2 report cycles + slack, named). Falls back to the plain pending
|
||||
CTA when escrowed, unstamped, or the stamp is unparseable.
|
||||
3. **Card branch.** `backups_remote.html`: one new escrow-card branch ahead of the existing chain —
|
||||
**info (blue `--blue`)** "Helyreállítási kód létrehozva … megerősítésre vár, legfeljebb 15 perc",
|
||||
degrading to **warn** "A megerősítés nem érkezett meg a várt időn belül …" + re-ceremony CTA past
|
||||
the window. Existing pending / stale (Scenario F) / escrowed branches untouched.
|
||||
4. **Wizard final screen.** `backups_escrow.html` "7. Befejezés" step gains the **"Mi történik
|
||||
ezután?"** info note (generated / sent to the hub / bounded ≤15-min wait) — the wizard finishes to
|
||||
`/backups/remote`, so the customer lands directly on the awaiting card.
|
||||
|
||||
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`.
|
||||
**NOT built (as instructed):** no scheduler rework; no manual report-trigger. *Observation:* no
|
||||
pre-existing manual/API report-trigger seam was found (report push is the scheduler `hub-report` job),
|
||||
so the one-liner "fire a report right after the ceremony" option was correctly skipped.
|
||||
|
||||
## 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).
|
||||
## Part 2 — hub v0.57.1: **NOT shipped** (Phase-0 = A, Hypothesis B false; see diagnosis above).
|
||||
|
||||
---
|
||||
|
||||
## Green gates & tests
|
||||
|
||||
- `go build ./...` OK · `go vet ./...` OK · `go test ./...` **0 failures**.
|
||||
- Design gates OK: `template_id_gate`, `emoji_gate`, `native_confirm_gate`, `offbox_rename_gate`.
|
||||
- New test `web/escrow_wait_state_test.go` — 8-case truth table + mutual-exclusion invariant.
|
||||
**Red-proof (run→fail→revert):** dropped `t.EscrowState == "escrowed"` from the early-return guard →
|
||||
`escrowed clears the wait` case FAILED (`awaiting=true, want false` — the interim card would resurface
|
||||
after the healthy state) → reverted → green.
|
||||
|
||||
## Scenarios
|
||||
|
||||
- **S-A (waiting state):** ceremony completes → info card shows immediately (stamp set on claim);
|
||||
confirming ACK zeroes the stamp AND sets escrowed → escrowed branch wins → healthy card. ✔ (logic +
|
||||
live: box is escrowed).
|
||||
- **S-B (timeout):** stamp aged past `escrowCeremonyGraceWindow` with state still pending → warn +
|
||||
re-ceremony CTA. ✔ (unit: `stamped past window → timed out`, `at boundary → timed out`).
|
||||
- **S-C (restart mid-wait):** stamp is persisted in `settings.json` → the info card survives a restart.
|
||||
✔ (persisted field; unit covers the stamped-pending path).
|
||||
- **S-D (hub, if B):** N/A — Phase-0 = A, no hub change.
|
||||
|
||||
## Deploy & live verification
|
||||
|
||||
- Committed to `main`; image `gitea.dooplex.hu/admin/felhom-controller:0.138.0` built+pushed on 180;
|
||||
demo guest 9201 update train **0.136.0 → 0.138.0** (two pending versions delivered cleanly).
|
||||
- Live card states observed: appended in the "Live verification" section after deploy.
|
||||
|
||||
**NOT done — Viktor's click:** the first "Távoli mentés most" (first offsite run) is the customer's, per
|
||||
the task. CC stops at escrowed-verified.
|
||||
|
||||
Reference in New Issue
Block a user