hub v0.47.0: CHANGELOG + README tab/host-lifecycle/offsite docs + REUSE map + manifest bump to 0.47.0
CONTEXT.md: v0.47.0 arc entry (resolves the 146d165 swept-WIP flag) + the
multi-endpoint-allocation deferral + pre-GitOps stale-template note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vvz1NCu22p8dGkRCpeX9re
This commit is contained in:
+20
-5
@@ -3,11 +3,26 @@
|
||||
> Created with the REUSE.md rollout (2026-07-03). Authoritative history: `hub/CHANGELOG.md` (hub),
|
||||
> `website/CHANGELOG.md`, `scripts/CHANGELOG.md`; end-of-task detail in `REPORT.md`.
|
||||
|
||||
- **⚠️ 2026-07-11 — hub v0.47.0 host-delete WIP rode along in commit `146d165`** (a docs commit's
|
||||
`git add -A` swept the uncommitted working tree: `store.go` DeleteHost + `hosts.go` handler +
|
||||
delete tests + host_detail_body template additions, continuing the `ae950e5` v0.47.0 arc). The
|
||||
full hub green gate passes on it and NOTHING deployed (manifest still 0.46.0) — but it shipped to
|
||||
main unreviewed by its author. Review/continue the v0.47.0 arc from there.
|
||||
- **2026-07-11 — HUB v0.47.0 UI REORGANIZATION SHIPPED** (CHANGELOG v0.47.0; commits `9f29bf3` →
|
||||
`0daddcd` + docs). Five deliverables: **(1)** `.data-table td a:not(.btn)` button-contrast fix;
|
||||
**(2)** customer page = **8 hash tabs** (`#tab=…`, sticky summary strip, Events error badge,
|
||||
no-JS graceful degradation — panels hide only via a JS-added body class); **(3)** shared
|
||||
`host_detail_body` sub-template rendered by `/hosts/{id}` AND the new per-customer **Host tab**
|
||||
(a list by design; `store.ListHostsByCustomer` + `hostDetailData` builder); **(4)** **stale host
|
||||
removal** — `GET /hosts/{id}/delete-impact` (counts/booleans only) + `POST /hosts/{id}/delete`
|
||||
behind type-to-confirm; ONLINE → 409 always (no override), escrow needs an explicit checkbox
|
||||
(`ErrHostEscrowPresent`, tx never starts), one-tx cascade incl. the bound wg peer (wgsync's 5-min
|
||||
declarative push converges the endpoint; log bundles die by `scope_id == host_id` only);
|
||||
**(5)** **/offsite multi-endpoint management UI** — all `wg_endpoints` rows as cards +
|
||||
add/edit/delete with 409 guards (peers-in-subnet pins subnet + delete). **Deferral (explicit):
|
||||
peer allocation, the wgsync reconciler push and the desired-state merge stay lowest-endpoint-id
|
||||
(`GetWGEndpoint` untouched); the future arc is a `wg_peers.endpoint_id` migration + per-endpoint
|
||||
allocation/reconciler/desired-state.** Five red-proofs ran (online gate, escrow ack, bundle
|
||||
scope, endpoint-delete guard, subnet-change guard). *Resolved:* the earlier ⚠️ about `146d165`
|
||||
sweeping the Part-4 WIP — the sweep caught `hosts.go` mid-red-proof (escrow ack bypassed);
|
||||
`068427a` restored the gate; the arc is now complete and author-reviewed. **Stale-doc note:** the
|
||||
workspace-root CLAUDE.md's hub deploy wording predates GitOps — deploys are manifest-tag bumps +
|
||||
deliberate ArgoCD sync, never `kubectl set image`.
|
||||
|
||||
- **2026-07-11 — RCA FIXES 1+2+4 SHIPPED + Q1c GREEN (agent v0.84.0 + controller v0.117.0 LIVE on
|
||||
demo).** Reboot survival: automatic since agent 0.84.0, live-proven 2026-07-11 21:20 (`pct reboot
|
||||
|
||||
@@ -55,6 +55,16 @@
|
||||
| `parseSQLiteTime` | hub/internal/store/store.go (~L1160) | `(s string) time.Time` | Parsing ANY timestamp read from SQLite | modernc/sqlite returns multiple formats; raw `time.Parse` will intermittently zero out. Always use this. |
|
||||
| `compareVersions` | hub/internal/web/server.go (~L571) | `(a, b string) int` | X.Y.Z comparisons in web (floor checks, update-available) | Returns 0 on parse error — unparseable compares as "equal" (see §3). |
|
||||
|
||||
### Host views & lifecycle / offsite endpoints (v0.47.0, hub/internal/web + store)
|
||||
|
||||
| Symbol | File | Short signature | Use for | Gotchas |
|
||||
|---|---|---|---|---|
|
||||
| `(*Server).hostDetailData` | hub/internal/web/hosts.go (~L282) | `(host *store.Host, r) map[string]interface{}` | The ONE view-model builder for the shared `host_detail_body` sub-template (standalone `/hosts/{id}` + customer Host tab) | Booleans/counts only for DR/escrow; carries `Deletable` (= status != "ok") which gates the danger-zone card. Never add a secret field. |
|
||||
| `host_detail_body` sub-template | hub/internal/web/templates/host_detail_body.html | `{{template "host_detail_body" .}}` | Rendering a host's detail sections on ANY surface | One namespace across ParseFS (icons.html pattern). Renders per-host — id-suffix any new element ids with `{{.HostID}}` (the customer page renders N instances). |
|
||||
| `(*Store).ListHostsByCustomer` | hub/internal/store/store.go (~L1620) | `(customerID) ([]Host, error)` | A customer's hosts, host_id order | A LIST by design (HA-cluster roadmap) — don't collapse to GetHostByCustomer. |
|
||||
| `(*Store).CountHostArtifacts` / `DeleteHost` | hub/internal/store/store.go (~L1640/~L1690) | `(hostID) (HostArtifacts, error)` / `(hostID, deleteEscrow bool) error` | Host-delete impact preview + the ONE-transaction cascade | ONLINE gate lives in the handler, escrow gate in the store (`ErrHostEscrowPresent`, tx never starts). log_bundles die by `scope_id == host_id` ONLY (customer-scoped bundles survive). The wg_peers delete is INSIDE the tx — never split it out. |
|
||||
| `(*Store).ListWGEndpoints` / `DeleteWGEndpoint` | hub/internal/store/wg.go (~L64/~L86) | `() ([]WGEndpoint, error)` / `(endpointID) error` | The /offsite endpoint-management surface | `GetWGEndpoint` (lowest id, LIMIT 1) stays THE allocation/sync endpoint — do not switch allocator/reconciler/desired-state to the list without the `wg_peers.endpoint_id` migration arc. Peers-in-subnet guards live in web/offsite.go. |
|
||||
|
||||
### Artifact manifest / Day-0 trust root
|
||||
|
||||
| Symbol | File | Short signature | Use for | Gotchas |
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## 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) — `<a class="btn">` 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-`<button>` pin and the detail-page 2-button pin stay green unmodified.
|
||||
- **Offsite multi-endpoint UI** (`store/wg.go` `ListWGEndpoints`/`DeleteWGEndpoint`,
|
||||
`web/offsite.go`, `templates/offsite.html`): `/offsite` lists ALL `wg_endpoints` rows as
|
||||
cards + add/edit/delete forms (posture change from S2 read-only — operator decision).
|
||||
Validation → 400 stores nothing; subnet edit / endpoint delete refused 409 while peers sit
|
||||
in the (current) subnet; pubkey change gets a type-to-confirm noting pull-based
|
||||
convergence. Peer table gains an Endpoint column (first id-ordered subnet match, em dash
|
||||
when none). **Allocation, reconciler push and desired-state merge stay lowest-endpoint-id**
|
||||
(`GetWGEndpoint` untouched; the page states the deferral) — per-endpoint allocation
|
||||
(`wg_peers.endpoint_id` migration) is a future arc.
|
||||
- **Tests**: +21 new/amended across web+store: tab render (no-JS completeness, badge, banner-
|
||||
above-tabs), Host tab shared-body/empty/isolation, delete cascade + refusal non-effects +
|
||||
impact shape + danger-card gating, offsite cards/column/validation/guards. Five companion
|
||||
red-proofs ran and FAILED as required (online gate, escrow ack, bundle scope, endpoint
|
||||
delete guard, subnet-change guard). Note: commit `146d165` (parallel session) swept the
|
||||
Part-4 WIP mid-red-proof — `068427a` restored the escrow-ack line.
|
||||
|
||||
## v0.46.0 — observability pass: per-box log pulls, bundle custody, TTL + secret gate (2026-07-11)
|
||||
|
||||
Hub third of the cross-repo observability task (agent v0.83.0 + controller v0.116.0): remote,
|
||||
|
||||
+24
-4
@@ -232,11 +232,31 @@ Synchronizer-token CSRF protection on all browser POST/DELETE/PATCH operations:
|
||||
- **Customers (`/configs`)** — Customer management list. Shows all customers (both managed and manual), their status, controller version, and config type (MANAGED/MANUAL). Blocked customers shown grayed-out with BLOCKED badge.
|
||||
- **Fleet App Analytics (`/apps`)** — Fleet-wide app telemetry overview (v0.4.0+). Shows all deployed apps across all customers with deployment count, avg/P95 memory, catalog estimate/limit accuracy indicators, and 24h error/warning badge counts. Sortable columns (deployments/memory/errors), 24h/7d/30d time period selector.
|
||||
- **App Detail (`/apps/{name}`)** — Per-app drill-down page with Chart.js memory trend (avg + peak lines, catalog limit dashed line), per-customer breakdown table, and known log issues table (severity, message, occurrence count, affected customers, first/last seen). Shows suggested mem_limit from P95×1.2 rounded to 32 MB.
|
||||
- **Unified Customer Detail (`/customers/{id}`)** — Single page per customer combining config management and live monitoring. Auto-refresh toggle (localStorage-persisted, enabled by default) replaces the previous hardcoded 60s meta-refresh. Adapts content based on available data:
|
||||
- **Managed + reporting:** Full view — config info, system metrics, storage, containers, backup status, events timeline (last 50, severity filter), credentials, setup commands, YAML preview, controller update, notifications (with channel column), history
|
||||
- **Managed + no reports yet:** Config info, credentials, setup commands, "Waiting for first report" indicator
|
||||
- **Manual (report-only):** System metrics, storage, containers, backup, with "Create Config" button to convert to managed
|
||||
- **Unified Customer Detail (`/customers/{id}`)** — Single page per customer combining config management and live monitoring. Auto-refresh toggle (localStorage-persisted, enabled by default) replaces the previous hardcoded 60s meta-refresh. Since v0.47.0 the sections are organized into **8 client-side hash tabs** (`#tab=<name>`, deep-linkable, survives auto-refresh) under a sticky summary strip (name, status, controller version, last report, containers). Panels hide only via a JS-added body class — with JS off every section renders stacked (graceful degradation). Tab map:
|
||||
- **Overview** (default) — customer info, health/issues/warnings, system metrics, storage, backup
|
||||
- **Applications** — containers, app telemetry, received app log tails
|
||||
- **Setup** — credentials, setup command generator, YAML preview
|
||||
- **Settings** — controller update + version floor, geo-restriction
|
||||
- **Backup & DR** — DR recipe panel + download
|
||||
- **Events** — events timeline (severity filter) + report history; the tab label carries a red count badge when error events exist (last 24h)
|
||||
- **Notifications** — prefs + recent notification log
|
||||
- **Host** — the customer's enrolled host(s), rendered via the shared `host_detail_body` sub-template (a list by design: 1 today, N for a future HA cluster) + cross-link to `/hosts/{id}`
|
||||
- **Config Form (`/configs/new`, `/configs/{id}/edit`)** — Create/edit customer configurations with identity, infrastructure tokens, and monitoring overrides. Legacy Monitoring UUIDs section collapsed by default with deprecation notice. CF API token requires **Zone DNS:Edit** (ACME) and **Zone WAF:Edit** (geo-restriction) permissions.
|
||||
- **Hosts (`/hosts`, `/hosts/{id}`)** — fleet list (read-only, zero buttons — pinned by test) + per-host detail (identity, vitals, guests, storage targets, log-bundle diagnostics, DR/escrow presence). The detail body is the shared `host_detail_body` sub-template also rendered on the customer page's Host tab.
|
||||
- **Offsite (`/offsite`)** — WireGuard endpoint cards + peer registry, with endpoint management since v0.47.0 (see below).
|
||||
|
||||
### Host lifecycle — stale host removal (v0.47.0)
|
||||
|
||||
Enrollment mints the host's API key exactly once (Day-0 passphrase flow); host reports authenticate via that key, so deleting a host row permanently bricks its heartbeat channel. The delete flow is therefore gated:
|
||||
|
||||
- The **danger-zone card** renders only for non-online hosts (stale / down / no-report). An ONLINE host is never deletable — `POST /hosts/{id}/delete` returns 409 unconditionally; no override exists.
|
||||
- `GET /hosts/{id}/delete-impact` returns the blast radius as counts/booleans only (guests, reports, host-scoped log bundles, escrow/wg-peer/PBS-secret/recovery presence) — never a secret or blob.
|
||||
- The dialog requires **retyping the host id**; when a key escrow exists, an unchecked-by-default checkbox ("also delete the key escrow + DR bundle") must be ticked — otherwise 409 and the transaction never starts (`store.ErrHostEscrowPresent`).
|
||||
- `store.DeleteHost` cascades in ONE transaction: guests, host_reports, signed_jobs, host_recovery, host_pbs_secrets, log_bundle_requests/log_bundles with `scope_id == host_id` (customer-scoped bundles are untouched), the bound wg_peers row, host_escrow (only when acked), then the hosts row. The wgsync reconciler's 5-minute declarative push converges the endpoint after the peer row disappears.
|
||||
|
||||
### Offsite endpoint management (v0.47.0)
|
||||
|
||||
`/offsite` lists **all** `wg_endpoints` rows as cards and can add/edit/delete them (`POST /offsite/endpoints`, `POST /offsite/endpoints/{id}/delete`). Guards: full field validation (CIDR subnet, PBS IP inside subnet, port 1–65535, pubkey non-empty, id `[a-z0-9-]+`) → 400 stores nothing; changing an endpoint's `tunnel_subnet` or deleting it is refused with 409 while any peer's /32 lies inside the (current) subnet; a server-pubkey change requires a type-to-confirm (peers converge on their next desired-state pull). The peer table shows each peer's containing endpoint. **Scope guard:** peer allocation, the wgsync reconciler push, and the desired-state merge still use the lowest `endpoint_id` only (`GetWGEndpoint`) — per-endpoint allocation (`wg_peers.endpoint_id`) is a deferred future arc.
|
||||
|
||||
### Customer States
|
||||
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: hub
|
||||
image: gitea.dooplex.hu/admin/felhom-hub:0.46.0
|
||||
image: gitea.dooplex.hu/admin/felhom-hub:0.47.0
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
|
||||
Reference in New Issue
Block a user