hub v0.27.0: Hosts page — read-only fleet view (audit F-M1)

New Hosts nav section: a fleet list (/hosts) + per-host detail (/hosts/{id}),
read-only (GET only, no host actions). Surfaces identity, agent version,
online/stale status (reusing the HostStalenessChecker threshold), guests,
vitals, storage targets with SMART/thin-pool, and DR/escrow presence.

- store: new ListGuestsForHost reader (reality cols only; omits api_key/
  desired_spec_json) + scanGuest helper.
- web: handleHostsList + handleHostDetail (hosts.go); hosts.html +
  host_detail.html; Hosts nav link on every page; timeAgoPtr helper; routes.
- tests: store getter, both handlers, no-secret (api_key) assertion, 404,
  no-report empty state, status-band mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 06:10:02 +02:00
parent 73b3f6ac71
commit 2289fc907c
15 changed files with 932 additions and 1 deletions
+39
View File
@@ -1,5 +1,44 @@
# Felhom Hub — Changelog
## v0.27.0 — Hosts page: read-only fleet view (audit F-M1) (2026-07-01)
Resolves audit finding **F-M1**: the agent enrolls as a *host* and the hub stores rich host state
(identity, agent version, guests, storage targets with SMART, DR/escrow, staleness) and alerts on it —
but the whole host domain was **invisible in the GUI** (email-only). Adds a **Hosts** nav section — a
fleet list + a per-host detail page. **Read-only** (GET only, no host actions/mutation routes): this
surfaces state the way the pull/desired-state model demands; it does not reintroduce inbound control
(retired in v0.26.0).
- **New store reader `ListGuestsForHost(hostID)` (`internal/store/store.go`).** `SELECT … FROM guests
WHERE host_id = ? ORDER BY vmid`, via a new `scanGuest` helper over `guestRealitySelectCols` — the
reality columns only. It deliberately **omits the secret/inert columns** (`api_key`,
`desired_spec_json`), so the read-only view can never surface them. Returns `[]` (never nil-error) on
no guests. (There was previously no guests *reader* — only `UpsertGuestFromReport`.)
- **New handlers (`internal/web/hosts.go`).**
- `handleHostsList` — `ListHosts` + a per-host status badge from `hostStatus()` (which reuses
`s.staleThreshold` — the **same** thresholds as the `HostStalenessChecker`: stale after the
threshold, down at 2× — so the badge agrees with the alerting) + per-host guest counts
(`ListGuestsForHost`) + vitals parsed from `GetLatestHostReportJSON` + worst storage fill grouped
from `GetHostStorageTargets`.
- `handleHostDetail` — `GetHost` (404 if absent) + `ListGuestsForHost` + rich storage targets (role,
state, fill %, thin-pool, SMART health/temp/wear parsed from the latest report body) + vitals +
`GetHostDRBundle`/`GetHostEscrow` **presence booleans only** (never the opaque blobs). Nil/missing
(no report, no guests, no storage, no DR) render empty states — never a panic.
- **New templates `hosts.html` + `host_detail.html`** (existing dark operator-console styling reused —
`data-table`, `status-badge-*`, `info-grid`, `empty-state`; no restyle). A no-report host shows a
STALE/NO-REPORT badge and "waiting for first report".
- **Nav:** added the `Hosts` link (between Apps and Configuration) to every page's `<nav>` (the nav is
duplicated per page, not a shared partial) + the new `timeAgoPtr` template helper for `*time.Time`.
- **Routes (`internal/web/server.go`):** `GET /hosts` (+ `/hosts/`) → list, `GET /hosts/{id}` → detail,
modelled on the `/apps` pair. GET only.
- **Tests:** `ListGuestsForHost` (none→empty, multiple→vmid-ordered, secret column not surfaced); the
list handler (N rows, ONLINE + NO-REPORT badges, worst-fill, no action buttons); the detail handler
(guests + storage + SMART + DR present, customer cross-link, **no-secret assertion** that the host
`api_key` is absent from the rendered body, no buttons); unknown host → 404; no-report host renders
the waiting state. `hostStatus` band mapping unit-tested (pending/ok/stale/down).
- **Remaining audit follow-ups (not this slice):** controller-side geo intent sync; a read-only
reported-vs-desired "Show Diff"; the cosmetic `controllerURL` cleanup in `configs.go`.
## v0.26.0 — pull-based config delivery + retire the inbound GUI controls (2026-06-30)
Closes audit `documentation/audits/AUDIT-hub-gui-2026-06-30.md` F-S1/F-S4 + the dead-template findings,