docs: v0.122.0 customer-claim gate — REPORT + CONTEXT

This commit is contained in:
2026-07-12 18:56:29 +02:00
parent 3cf49c7fd5
commit d4641c221d
2 changed files with 56 additions and 90 deletions
+16 -1
View File
@@ -7,7 +7,22 @@
>
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
Last updated: 2026-07-12 (v0.121.0 — backups page truth pass)
Last updated: 2026-07-12 (v0.122.0 — customer-claim password gate)
> **2026-07-12 — v0.122.0: CUSTOMER-CLAIM PASSWORD GATE (closes DRILL-day0-vm F-4/F-5). MinAgent
> 0.81.0 unchanged. Requires hub v0.50.0.** The dashboard password is CUSTOMER-OWNED via a one-time
> claim code the hub emails to the registered address — the "no password → open dashboard" default
> is GONE. Unclaimed box (code hash delivered, no password) → serves ONLY `/claim`; every other route
> → claim page (302) or 401 (API). A set password disables the gate (auth wins). Reset rides the same
> code engine (login "Elfelejtett jelszó"). Legacy-open (no password + no hash) → red transition
> banner until the hub delivers a hash. `internal/web/claim.go` (gate + pages + HMAC pre-auth CSRF +
> 5-try/15-min lockout → `claim_lockout` event), `report/claim_sync.go` (ACK cache, idempotent by
> generation), settings `Claimed`/`ClaimCode*`/`ClaimConsumedGeneration`, `config.web.claim_code_*`
> (hub-baked), `--print-reset-code` root hatch. Gate-coverage signature test + 4 red-proofs.
> **LIVE-PROVEN on drill guest 9201 (0.122.0): gate ON via the real edge (/ → 302 claim page, /api →
> 401), code emailed to demo-vm-felhom's registered address.** Floor raise 0.120→0.122 = operator's
> LAST step (supervised). Details: felhom.eu/documentation/audits/DRILL-day0-vm-2026-07-12.md (F-4/F-5
> RESOLVED).
> **2026-07-12 — v0.121.0: BACKUPS PAGE TRUTH PASS. MinAgent 0.81.0 unchanged. Controller-only, no
> agent-API change.** Pure UI/data-plumbing on `/backups`; no backup-engine behavior change. Fixes the
+40 -89
View File
@@ -1,98 +1,49 @@
# REPORT — v0.121.0: backups page truth pass
# REPORT — v0.122.0: customer-claim password gate (closes DRILL-day0-vm F-4/F-5)
**Date:** 2026-07-12 · **Version:** controller v0.121.0 (from v0.120.0) · **MinAgent:** 0.81.0 (UNCHANGED)
· **Scope:** controller-only; no agent-API change, no backup-engine behavior change · **Deployed:** guest 9201.
**Confirmed baseline:** `felhom-controller` @ `92d670e8a6` (main), v0.120.0 → v0.121.0.
## Problem
On the live demo (`/backups`, v0.120.0) the page contradicted itself: a dead "Részletek" card claimed
"Nincs 2. szintű mentés konfigurálva" (the never-set `.Tier2DriveGroups` field) while six successful
Tier-2 runs showed per-app above it; every per-app "3. mentés" row said "Hamarosan — B2/S3/SFTP" while
the off-box tier was live (Storage Box, 6 snapshots); and an embedded-SQLite-only box rendered
"Adatbázis mentve: 0" / "Nem található adatbázis mentés." as if backups were failing.
**Date:** 2026-07-12 · **Version:** controller v0.122.0 (from v0.121.0) · **MinAgent:** 0.81.0 (UNCHANGED) · **Requires:** hub v0.50.0
## What shipped
1. **Dead "Részletek" card removed** — operator-approved as redundant (per-app rows + Adatbázisok
section carry the truth). Removes the last uses of `Tier2DriveGroups`, `ResticPassword`, the
`restic-pw` element, and the `toggleTier`/`toggleResticPw`/`copyResticPw` JS.
2. **Per-app "3. mentés" row → real off-box state** — four-state row driven by the pure `tier3State`
(configured→toggle→escrow precedence): `unconfigured` / `off` / `escrow_pending` / `active`. Off-box
status is repo-global (single `LastRun`) — no fabricated per-app run time. "hamarosan" gone.
3. **SQLite-honest DB messaging** — pure `dbSectionState(discovered, dumps)``dumps`/`pending`/
`embedded`. Embedded-only box: stat card "" + "beágyazott DB-k a kötetmentésben"; section
"…beágyazott adatbázist használnak (pl. SQLite)…". Discovered-not-dumped: "…az első ütemezett mentés
éjjel fut le."
4. **Dead/raw display fields fixed**`Tier1LastRun`/`Tier1LastStatus` populated from
`ListRestorePoints` (correct per-drive resolution; no time for unit-less apps); Tier-1/Tier-2 labels
via `timeAgoStr` (relative), confirm() dialog keeps the precise timestamp.
5. **Terminology split** — off-box section retitled "Távoli mentés (3. mentés) — titkosított, offsite"
(+ `#offbox-section` anchor); whole-guest PBS stat card relabeled "Távoli rendszermentés".
6. **Deploy page** — app backup card gains "Mentési beállítások →" → `/stacks/<name>/backup`.
The dashboard password is now **customer-owned**, set through a one-time claim code the hub emails
to the registered address. The old "no password → open dashboard" default is gone: a fresh box
(claim-code hash delivered, no password) serves ONLY the claim page — every other route answers the
claim page (`302 → /claim`) or `401` (API). This closes the DRILL-day0-vm F-4 exposure (every Day-0
dashboard was open on the internet, with the full deploy API = RCE-equivalent) and F-5 (the
unauthenticated geo toggle is now behind the gate too).
## Files
- **`internal/web/claim.go`** — `claimGateActive()` / `effectiveClaimCode()` (ACK-cached settings
beats the config bake by generation), the claim page (`GET /claim`), submit (`POST /claim`:
verify code → set own password via `SetPasswordHash` → mark claimed → consume generation →
session), and `POST /claim/request-new-code` (forwards to the hub reset-request). Code checks:
bcrypt match AND generation-not-consumed (single-use) AND ≤ 72 h. Per-source + global limiter (5
→ 15-min lockout) raising the allowlisted `claim_lockout` event. Pre-auth CSRF = HMAC over
`web.session_secret` (fixes CTRL-007). Min password 12.
- **Gate wiring** (`auth.go`/`csrf.go`/`server.go`/`cmd`): gate atop `RequireAuth`; a SET password
disables it (auth wins — claimed boxes never regress). `/claim*` + `/static/*` reachable pre-auth.
Legacy-open (no password, no hash) → red transition banner (`layout.html`). Login "Elfelejtett
jelszó" link.
- **`internal/report/claim_sync.go`** — caches the ACK `claim {hash, generation}` into settings.json
idempotently by generation (one-way: newer advances; same/older/nil never rewrites; hub outage
never clears). Report carries `claimed` (set-only). Hub bakes `web.claim_code_*` for gate-from-
first-boot.
- **`--print-reset-code`** root escape hatch (one-time local code above cached/baked/consumed gen).
- **Created:** `controller/internal/web/backup_page_state.go` (pure `dbSectionState`, `tier3State`);
`controller/internal/web/backup_page_state_test.go` (9 tests).
- **Modified:** `controller/internal/web/handlers.go` (`AppBackupRow` +`OffboxEnabled`/`Tier3State`;
`buildAppBackupRows` Tier-1 + Tier-3 plumbing; `backupsHandler` `DBSectionState`);
`controller/internal/web/templates/backups.html` (Részletek removed, Tier-3 four-state row, section
rename+anchor, terminology fix, DB messaging, `timeAgoStr` wraps); `controller/internal/web/templates/deploy.html`
(settings link); `CHANGELOG.md`, `REPORT.md`, `CONTEXT.md`, `controller/README.md`.
## Tests + red-proofs
## Tests
- Gate-coverage signature test (`TestClaimGate_EveryRouteGated`): every route → claim page/401, a
deploy POST reaches no handler. Plus happy-path / reuse-refused / expired / lockout+window-reopen
(fake clock), legacy-open vs password-set, claim-sync idempotence.
- **Four §10 red-proofs proven** (mutate → FAIL → revert → green): (1) gate skip-line, (2) single-use
generation — hub `RotateClaimCode` bump AND controller consumed-generation check, (3) reset
non-DoS (clearing claimed → fail), (4) rate-limiter (both scopes disabled → fail).
- Green gate: `go build ./... && go vet ./...` clean; all touched packages pass. Pre-existing
env-sensitive `internal/backup` volume-dump failures are unrelated (fail identically on the clean
baseline — no tar/zstd toolchain on the dev box).
- **New (9, all green):** `TestDBSectionState` (table), `TestTier3State` (table, all 4 states +
precedence), `TestBuildAppBackupRows_OffboxMapping` (Scenario A), `_EscrowPendingPrecedence`
(Scenario B), `_Tier1FromRestorePoints` (Scenario D — unit→RFC3339, no-unit→""),
`TestBackupsTemplate_Tier3Live` (Scenario A + removal assertions), `_Tier3EscrowPending` (Scenario B),
`_DBMessaging` (Scenario C embedded/pending), `_Tier2RelativeTime` (Scenario D).
- **Test count (internal/web):** 108 → 117 test funcs.
- **Companion red-proofs (run → FAIL → revert), all verified:**
1. Re-insert "hamarosan elérhető" into the rendered Tier-3 `off` block → `TestBackupsTemplate_Tier3Live`
FAILED on the no-"hamarosan" assertion → reverted. ✓
2. Hardcode `row.OffboxEnabled = false` in `buildAppBackupRows``TestBuildAppBackupRows_OffboxMapping`
FAILED (calibre OffboxEnabled:false, Tier3State:off) → reverted. ✓
3. `dbSectionState` drops the `discovered` branch → `TestDBSectionState` FAILED on `(2,0)` want "pending"
→ reverted. ✓
4. Drop the `row.Tier1LastRun = pts[0].Time` assignment → `TestBuildAppBackupRows_Tier1FromRestorePoints`
FAILED (hasunit Tier1LastRun "") → reverted. ✓
- **Green gate:** `go build ./...` + `go vet ./internal/web/` + `go test ./internal/web/` all green.
Template gates `template_id_gate.py` + `emoji_gate.py` OK. (Pre-existing, unrelated: `internal/backup`
`TestDumpAppVolumes_*` fail in this environment — docker-dependent; confirmed failing on the clean
`92d670e` baseline too, and I did not touch `internal/backup`.)
## Live validation (drill guest 9201 on qm 300, controller 0.122.0, pre-floor)
## Deploy & live validation
- **Built + pushed** on 180: `gitea.dooplex.hu/admin/felhom-controller:0.121.0`
(digest `sha256:0478d42a…`, 145M). **Deployed** to guest 9201 via the bootstrap flow.
- **`docker ps`:** `gitea.dooplex.hu/admin/felhom-controller:0.121.0 Up (healthy)`;
log: `Event pushed: controller_started (info) — Controller elindult (0.121.0)`. Version badge on
the page footer reads `0.121.0`.
- **Live UI validation (claude-in-chrome, `https://felhom.demo-felhom.eu`):**
- (a) **no "hamarosan" anywhere** on `/backups` — confirmed (page text + expanded rows). ✓
- (b) **Calibre-Web Tier-3 row** = "Sikeres" + "restic → u629488-sub1.your-storagebox.de" +
"Utolsó: 13 órája" + "Helyreállítási egység, titkosítva". ✓
- (c) **Radarr (untoggled) Tier-3** = "Kikapcsolva" + "Ez az alkalmazás nincs kijelölve távoli
mentésre" + "Bekapcsolás"; clicking it jumped the URL to `…/backups#offbox-section`. ✓
- (d) **"Részletek" card gone.** ✓
- (e) **DB messaging** — stat card "Adatbázis mentve / beágyazott DB-k a kötetmentésben"; Adatbázisok
section "…beágyazott adatbázist használnak (pl. SQLite)…". ✓
- (f) **Relative times** — Tier-1 "Utolsó: most ✓", Tier-2 "Utolsó: 33 perce Sikeres" (Calibre) /
"3 órája" (Radarr) — no raw RFC3339 in the visible labels. ✓
- (g) **Deploy page** (`/stacks/calibre-web/deploy`) shows "Mentési állapot → · Mentési beállítások →";
the new link opens `/stacks/calibre-web/backup` = "2. mentés beállítása" tier-2 config page. ✓
- Method: claude-in-chrome (server-side render exercised end-to-end; no pipeline bypass). Per §13,
no restore/format/offbox toggle button was pressed on the live demo.
## Observations (documented, NOT acted on)
- **Orphaned style.css classes** left by the Részletek removal (not cleaned per task §9.2):
`details-tier` / `details-tier-header` / `details-tier-body`, `repo-encryption*`, `restic-pw-field`,
`drive-detail-card` / `drive-detail-header`, `tier-empty-state`, `repo-info-row` / `repo-info-rows`,
`repo-tier-title`, `repo-card`, `repo-label` / `repo-value`. Candidates for a later CSS sweep.
- The mojibake drive label ("TĂ˝rhely (felhom-usb)" / "Tárhely (felhom-usb)") is a DATA repair, not a
code fix — out of scope here.
- `felhom.eu/documentation/controller/backup-architecture.md` off-box refresh is a separate queued doc task.
Deployed 0.122.0 → the ACK delivered the claim hash → gate flipped ON within a report cycle. Proven
via the REAL Cloudflare edge (`curl --resolve`): `/` → 302 → "A szerver beállítása" claim page;
`/claim` → 200; `/api/host-metrics` → 401. Controller log: `claim-sync: hub claim code cached
(generation 1)`. Hub log: claim code emailed to the registered address of demo-vm-felhom. The
customer-side claim + reset + the floor raise are the operator's supervised steps.