# 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) — `` 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-`