# Felhom Hub — Changelog ## v0.50.0 — customer-claim password arc: code engine + email + ACK/config delivery (2026-07-12) Hub half of the customer-claim password gate (closes DRILL-day0-vm F-4/F-5; needs controller v0.122.0). The customer OWNS the dashboard password — the hub generates a one-time claim code, emails it (Hungarian) to the REGISTERED address, and stores only `bcrypt(code)`. No operator-set path; the plaintext code exists solely inside the email send (the retrieval-passphrase custody rule). - **`internal/claim`** — the code engine. `EnsureIssued` (idempotent — issue+email at the FIRST real config retrieve = Day-0, and at a live box's first report; repeated pulls/reports never rotate or re-send), `Resend` (operator button; rotates generation — unclaimed gets the claim template, claimed gets the reset template), `RequestReset` (controller-forwarded "Elfelejtett jelszó", rate-limited 3/day/customer), `MarkClaimed` (set-only; one confirmation email on the unclaimed→claimed transition). - **`store.customer_claims`** — per-customer `{code_hash, generation, issued_at, emailed_at, claimed_at, reset_day, reset_count}`. `RotateClaimCode` bumps the generation (single active code) and PRESERVES `claimed_at` (a reset never un-claims); `MarkClaimed` is set-only. - **Delivery**: `GET /api/v1/config/{id}` bakes `web.claim_code_{hash,generation,issued_at}` into the generated controller.yaml (gate-from-first-boot) and issues the first code; the report ACK serves the active `claim {code_hash, generation, issued_at}` (allowlisted) and ingests the controller's `claimed` flag (set-only). `POST /api/v1/claim/reset-request` (self-scoped by the box's report key). New emails via the notify dispatcher; `claim_lockout` event allowlisted. - **UI**: the customer page Setup tab shows a claim status chip (Nyitott — kód kiküldve / Claimed) + a "Kód újraküldése" button (`POST /configs/{id}/claim-resend`) — no plaintext code ever rendered (there is none to render). - 15 tests (engine, ACK/config, UI); the arc's red-proofs live in the controller repo (gate) + here (generation bump, reset non-DoS). ## v0.49.0 — Edit tab merge (edit-a), scoped auto-refresh, style.css cache-bust (2026-07-12) > The task spec targeted "v0.48.0", but v0.48.0 (app_start_failed, below) had already shipped + > deployed by the time this train ran — a published tag is never re-pointed, so this is v0.49.0. > Baseline `3e949bc`; commits `e740147` → `2e03de1` → `1d94b1a` → docs/manifest. - **Edit tab merge (edit-a)** (`templates/customer_unified.html`, `templates/config_form.html`, new `templates/config_form_body.html`, `web/configs.go`, `web/pbsdr.go`): the standalone customer edit page merged into the customer page's Settings tab, renamed **Edit**. The form body is a shared `{{define "config_form_body"}}` sub-template (the `host_detail_body` pattern) built by the one `configFormData` view-model builder; the standalone chrome keeps rendering it for the create flow (`/configs/new`) and the validation-error re-render. The Edit tab renders: config form, Controller Update card, Geo card, and a **Danger zone** card holding the Block/Unblock/Delete forms relocated verbatim from the Customer Info header (endpoints + `confirm()` unchanged) — all SIBLINGS after `` (nested forms are invalid HTML and would break the offsite/PBS `formaction` sub-buttons). The header keeps only the config-less Create Config action. `GET /configs/{id}/edit` → 302 `/customers/{id}#tab=edit`; tabs JS gains the `settings`→`edit` legacy-hash alias. - **Server-side required fields on update** (`web/configs.go` `handleConfigUpdate`): the twin of the form's `required` attributes (Display Name + Domain), checked BEFORE provisioning; the error path re-renders the standalone page with the SUBMITTED overrides so typed values are never lost (red-proofed: nil overrides → values reset → test fails). - **Redirect anchors**: update/block/unblock/offsite-reissue/offsite-freeze/pbsdr-reissue → `?flash=…#tab=edit`; regen-password → `#tab=setup` (its card lives there); delete unchanged (`/configs?flash=deleted`). - **Scoped auto-refresh** (`templates/customer_unified.html`): the 60s reload fires only while a live tab (`data-live-tabs="overview,applications,events,host"` on the nav) is active AND no form is dirty (delegated document-level input/change listener, never reset — a reload clears it). Skipped ticks reschedule; a muted `(paused)` hint shows next to the toggle on non-live tabs / dirty forms. Toggle, `hub_auto_refresh` localStorage key, cadence, default-on: unchanged. - **style.css cache-bust** (all `templates/*.html`): every stylesheet link is now `/style.css?v={{hubVersion}}` — closes the v0.47.0 gotcha (`max-age=3600` served stale styling for up to an hour after each deploy). Red-proofed (bare link fails the render test). - **Repo staging rule** (`CLAUDE.md`): never `git add -A` in this repo (the v0.47.0 `146d165` sweep incident) — explicit paths, pull-rebase, one writing session per clone. - Tests: +11 (Group A panel surface / sibling-form / header-count, Group B redirect + create + typed-values-preservation table, Group C refresh structural pins, Group D cache-bust sweep). Amended pins: `customer_tabs_test.go` (settings→edit), `pbsdr_test.go` (postUpdate supplies the now-required fields; FormRendersState asserts the embedded Edit-tab render). ## v0.48.0 — accept the app_start_failed event (controller fix-3, CAMPAIGN-3) (2026-07-12) - `app_start_failed` added to `allowedEventTypes` (`internal/api/handler.go`) + `customerMessages` (`internal/notify/templates.go`). Without the allowlist entry the controller's fix-3 event (a DEPLOYED app found not running — controller v0.120.0) would 400 at ingest and never reach the operator. No other hub change; pairs with controller v0.120.0 which closes the CAMPAIGN-3 finding set. ## v0.47.0 — UI reorganization: customer tabs, Host tab, stale-host removal, offsite multi-endpoint UI, button contrast (2026-07-11) Five hub-side deliverables; no agent/controller/protocol changes. Baseline `8e1a3f0` (v0.46.0); commits `9f29bf3` → `ae950e5` → `146d165`(swept WIP) → `068427a` → `0daddcd`. - **CSS button contrast** (`templates/style.css`): `.data-table td a` → `:not(.btn)` (base + hover) — `` inside data-table cells (host-detail Diagnostics View/Download, customer log-tail buttons) rendered blue-bright on blue-bright, i.e. invisible. Plain table links keep the bright-link style; `.btn` itself untouched, no `!important`. - **Customer page tabs** (`templates/customer_unified.html`, `style.css`): the ~18 stacked sections split into 8 client-side hash tabs (`#tab=` overview / applications / setup / settings / backup / events / notifications / host) + a sticky summary strip (name, status, controller version, last report, containers chip). Graceful degradation is load-bearing: panels hide only under a JS-added `body.js-tabs` class — no JS = every section visible, all existing render tests pass unmodified. Events tab carries a red error-count badge (reuses the already-fetched `CountEventsBySeverity` data — no new query). The auto-refresh reload preserves the hash → the active tab survives. No handler/data-model change for the tabs. - **Host tab + shared sub-template** (`templates/host_detail_body.html`, `web/hosts.go`, `web/configs.go`, `store.ListHostsByCustomer`): the host-detail body extracted into a `{{define "host_detail_body"}}` rendered by BOTH `/hosts/{id}` (chrome + call) and the new per-customer Host tab (a LIST by design — 1 host today, N for a later HA cluster; empty state otherwise). `handleHostDetail`'s data assembly extracted into `hostDetailData`. - **Stale host removal** (`store.CountHostArtifacts`/`DeleteHost`, `web/hosts.go` handlers, routes above the `/hosts/` catch-all): `GET /hosts/{id}/delete-impact` (counts/booleans ONLY) + `POST /hosts/{id}/delete` behind a type-to-confirm dialog (global-floor pattern). Gates: ONLINE host → 409 always (no override — a live agent would 401 forever; enroll is passphrase-gated mint-once); confirm mismatch → 400; escrow present without the explicit checkbox → 409 with the tx never started (`ErrHostEscrowPresent`, fail-safe-to-refuse). One transaction cascades guests, host_reports, signed_jobs, host_recovery, host_pbs_secrets, host-scoped log bundles (`scope_id == host_id` ONLY — customer-scoped bundles survive), the bound wg peer (inside the tx — no stranded peer on crash), escrow (only when acked), then the host row. The wgsync 5-min declarative push converges the endpoint afterwards — no reconciler change. Danger-zone card renders only when deletable, so the hosts-list zero-`