# AUDIT — Hub web GUI vs. current architecture

- **Date:** 2026-06-30
- **Repo / baseline:** `felhom.eu` `hub/`, `main` @ `fdcbc04` — hub **v0.25.0** (CHANGELOG top).
  > The audit prompt cited `v0.16.0`; that is stale. Live build is v0.25.0. `hub/CLAUDE.md`
  > "current state (v0.7.x)" is *also* stale — see F-S6.
- **Method:** read-only source trace of every route in `internal/web/server.go` + its handler + its
  template, cross-referenced against the live API (`internal/api/handler.go`), the store
  (`internal/store/`), the host-install script (`scripts/felhom-host-install.sh`), the controller
  report builder (`felhom-controller/.../report/builder.go`), and the architecture docs
  (`documentation/architecture/01-topology-and-trust.md`).
- **Live-confirmed?** All findings below were confirmed by **source read**, not by loading the
  rendered page. `confirmed-live` is therefore **n** for every row; the one item that would most
  benefit from a live check is F-C1 (storage %).
- **Scope guard:** this pass ships **only this doc**. No GUI/template/handler code was changed and no
  version bumped, per the task's STOP rule.

---

## 0. Route → template → handler map (the served surface)

Routes that render an HTML page (from `server.go` `ServeHTTP` + the `ExecuteTemplate` calls):

| Nav | URL | Handler | Template |
|-----|-----|---------|----------|
| Dashboard | `/` | `handleDashboard` (server.go:444) | `dashboard.html` |
| Customers | `/configs` | `handleConfigList` (configs.go:59) | `configs.html` |
| Customers | `/customers/{id}` | `handleCustomerUnified` (configs.go:165) | `customer_unified.html` |
| Customers | `/configs/new`, `/configs/{id}/edit` | `handleConfigNewForm` / `handleConfigEditForm` | `config_form.html` |
| Apps | `/apps` | `handleApps` (apps.go) | `apps.html` |
| Apps | `/apps/{name}` | `handleAppDetail` (apps.go) | `app_detail.html` |
| Configuration | `/configuration` | `handleConfiguration` (server.go:664) | `configuration.html` |

`/configs/{id}` (any non-suffixed) **302-redirects to `/customers/{id}`** (server.go:305-308).
No `{{template "..."}}` partial-includes exist (grep clean), so a template is reachable **only** if a
handler names it in `ExecuteTemplate`.

**Two templates are named by no handler:** `customer.html`, `config_detail.html` → see F-D1/F-D2.

---

## 1. Findings

| ID | Cat | Location (file:line / page) | What's wrong | Recommendation | Effort |
|----|-----|------------------------------|--------------|----------------|--------|
| **F-S1** | STALE | `customer_unified.html:386-390` (LIVE — Customer detail → "Setup Commands" → Option 1) | Advertises `sudo ./docker-setup.sh --hub-customer … --hub-password …` — the **pre-Proxmox bare-Docker** install. The current Day-0 is the Proxmox host-bootstrap. Anyone following this is actively misled. | Replace Option 1 with the host-install command (see §2 "proposed copy"). Decide remove vs. keep-as-legacy (Open-Q1). | S |
| **F-S2** | STALE | `customer_unified.html:392-396` (LIVE — Option 2 "Direct download") | `curl …/api/v1/config/{id} -H "X-Retrieval-Password" -o controller.yaml` hits a **still-valid** endpoint, but the workflow is wrong for Proxmox: the operator no longer hand-places `controller.yaml`; the in-guest controller pulls it itself (bootstrap-v2 config-retrieve) after the agent provisions the guest. Label/context is stale, endpoint is not. | Re-label as "Manual config fetch (debug only)" or fold into the host-install flow. Keep the endpoint. | S |
| **F-S3** | STALE | `customer_unified.html:347` (LIVE — "Waiting for First Report") | "Use one of the setup commands below to deploy the controller on the customer node." — reinforces the SSH-into-a-customer-node mental model that Proxmox replaced. | Reword to point at the host-install Day-0 procedure. | S |
| **F-S4** | STALE / ARCH-DRIFT | `customer_unified.html` buttons: Trigger Update (475/479), Push Config (484), Pull Config (487), Show Diff (466), Geo-disable (297); handlers `handleTriggerUpdate` (server.go:525), `handlePushConfig`/`handlePullConfig`/`handleConfigDiff` (configs.go:722/1089/999), `notifyControllerGeoDisable` (configs.go:1286) | **All five make the hub connect *into* the box** (`controllerURL + /api/...`). The locked trust model says the opposite: `01-topology-and-trust.md:11` "**the hub never connects into a customer box**"; `:79` "the hub never initiates inbound." These are pre-Proxmox data-plane controls. Under the Proxmox model controller updates are **agent-driven** (version floor / `MaybeAutoUpdate`) and config is **box-pulled** (config-retrieve), so these buttons are at best a trust-model violation and at worst dead (the box is only reachable via its CF tunnel public URL, not the hub). | Decide per Open-Q2: retire these controls, or formally bless a documented exception. Until then flag them as legacy. **High-value** — this is the largest correctness/architecture gap. | M |
| **F-S5** | STALE | `dashboard.html:25`, `customer_unified.html:311`, `customer.html:141` | Empty/disabled hints say "Configure `hub.enabled: true` in customer `controller.yaml`". Still literally true for the controller, but the onboarding path is now host-install (which seeds it). Low harm. | Light reword once F-S1 lands; otherwise leave. | S |
| **F-S6** | STALE | `hub/CLAUDE.md:24` ("current state (v0.7.x)"), tables at `:26-29` | Project-instructions file describes a v0.7.x hub; live is v0.25.0 (host-disk/storage-fill alerting, artifact manifest, version floor, DR recipe all post-date it). Misleads the next contributor. | Refresh the "current state" section. (Not GUI, but surfaced by the audit.) | S |
| **F-D1** | DEAD | `templates/customer.html` (entire file, 324 lines) | The **old, non-unified** customer detail page. Superseded by `customer_unified.html`; named by no handler; not included anywhere. Also missing CSRF on its Trigger-Update fetch (line 223) and lacking floor / DR-recipe / app-telemetry sections — proof it's a stale fork. | Delete (after Open-Q confirms no out-of-tree reference). | S |
| **F-D2** | DEAD | `templates/config_detail.html` (entire file, 156 lines) | The old standalone config-detail page. `/configs/{id}` now redirects to `/customers/{id}` (server.go:305-308); named by no handler. Carries the **same** stale `docker-setup.sh` block (lines 107-117) as F-S1, so leaving it invites re-introduction. | Delete. | S |
| **F-M1** | MISSING | No route in `server.go`; store has `ListHosts`/`GetHost`/`GetHostStaleness`/`SaveHostReport` + `hosts`/`guests`/`host_reports` tables | **There is no host view at all.** The agent enrolls as a *host* and the hub runs `HostStalenessChecker` + `HostDiskChecker` + `StorageFillChecker` over host-reports (→ email alerts), but the operator cannot **see** any host, its guests, its storage targets, enrollment status, or host-down state in the UI. The whole host domain is invisible except via email. | Add a Hosts page (list hosts + status + guests + storage-target fill). Biggest missing surface. | L |
| **F-M2** | MISSING | `customer_unified.html` "Controller Update" section (408-496) | No surfacing of **agentic update state / last result**. The page shows current version, registry-latest, floor, and a "below floor — will auto-update" hint, but never "last update: 0.86.0→0.87.0 (auto-floor) at …" or success/failure. (The prompt's referenced "Utolsó frissítés" line **does not exist** in the code — grep clean.) | Add a last-update-result row sourced from report/event data. | M |
| **F-M3** | MISSING (minor) | Host-domain ops APIs (escrow, desired-state, signed jobs, recovery-mode, re-enroll, restore-directive — see CHANGELOG) | Rich operator APIs exist with **zero** GUI; all are global-key/CLI-only today. Likely intentional (operator CLI), but worth an explicit "GUI not planned" note so it isn't mistaken for a regression. | Document as CLI-only, or add read-only visibility under the F-M1 Hosts page. | M |
| **F-C1** | CORRECTNESS | `customer_unified.html:166-177` (Storage); source `report/builder.go:93,122` | The template binds `index . "percent"` / `used_gb` / `total_gb`. Field **names are correct** — the controller emits exactly `percent`/`used_gb`/`total_gb` (`StorageReport{… Percent: sysInfo.DiskPercent}`). So a "0% bar with non-zero sizes" is a **data/source** issue (`sysInfo.DiskPercent` computed 0, or a genuinely near-empty fresh drive), **not** a template-binding bug. | No hub change. If 0% recurs on a non-empty mount, chase it controller-side (`system/info_linux.go` `readDiskUsage`). Verify live to close. | S |
| **F-P1** | POLISH | `customer_unified.html` (whole page, ~956 lines) | One very long single-column scroll: Info → System → Storage → Containers → Backup → Geo → Health → Credentials → Setup → YAML → Controller Update → Events → App Telemetry → DR → Notifications → History. Dense for an operator hunting one fact. | Optional: group into collapsible `<details>` or tabs (Overview / Config & Setup / Updates / Telemetry / DR). Suggestion, not a mandate. | M |
| **F-P2** | POLISH | HU/EN mix on `customer_unified.html` | Page chrome is English, but the **Geo-restriction** section (239-301) and its confirm dialogs (707-735) and the `config_form.html:80` hint are Hungarian. Operator UI is otherwise English. | Pick one language for the operator console (English) and translate the Geo block, or accept the mix deliberately. | S |
| **F-P3** | POLISH | `customer_unified.html:444` | Floor unset renders literal "**none (Phase 2 inert)**" to the operator — internal phrasing ("Phase 2 inert") leaks implementation jargon into the UI. | Reword to "none (no floor set)". | S |

---

## 2. Prioritized shortlist — fix first

1. **F-S1 (+F-D2) — the `docker-setup.sh` setup command.** It actively misleads anyone deploying and
   is the clearest "fix-now". Cheap, high-impact. Removing/relabelling the dead `config_detail.html`
   (F-D2) in the same pass stops the stale block from creeping back.
   *Proposed copy for the live "Setup Commands" card:*
   - **Option 1 — Day-0 host bootstrap (recommended).** On a freshly-PVE-installed Proxmox host, as
     root:
     `sudo ./felhom-host-install.sh --customer-id {{.CustomerID}}`
     (the retrieval passphrase is entered at the no-echo prompt — never on the command line;
     `--passphrase-file` for automation). This enrolls the host, installs+verifies the agent, and
     provisions the guest; the in-guest controller then pulls its own `controller.yaml`.
   - **Option 2 — Manual config fetch (debug only).** `curl -fsSL
     https://hub.felhom.eu/api/v1/config/{{.CustomerID}} -H "X-Retrieval-Password: …" -o
     controller.yaml` — the same payload the controller pulls itself; for inspection, not normal
     provisioning.
2. **F-S4 — the inbound data-plane buttons.** Largest architecture gap; decide retire-vs-bless
   (Open-Q2) before anyone relies on a button that the trust model forbids.
3. **F-M1 — Hosts page.** The single biggest *missing* surface; host health is invisible in the UI.
   Larger effort, so plan it deliberately rather than rushing.
4. **F-D1 — delete `customer.html`.** Zero-risk cleanup once references are confirmed.

---

## 3. Dead-code list (orphaned, safe to remove — flag, don't delete in this pass)

| File | Evidence it's dead |
|------|--------------------|
| `templates/customer.html` | Not named by any `ExecuteTemplate` (grep: only dashboard/customer_unified/config_form/configs/apps/app_detail/configuration/dr are). No `{{template}}` include. Superseded by `customer_unified.html`. |
| `templates/config_detail.html` | Same — no handler names it. `/configs/{id}` redirects to `/customers/{id}` (server.go:305-308). |

`configs.html` / `configuration.html` / `config_form.html` were checked for the overlap the prompt
flagged: **all three are live and distinct** — `configs.html` = Customers list (+global floor +artifact
manifest forms), `configuration.html` = the "Configuration" nav page (asset re-seed), `config_form.html`
= the new/edit customer form. No dead code among them.

---

## 4. Missing-controls list (shipped backend, no/weak GUI)

| Capability | Backend | GUI today | Gap |
|-----------|---------|-----------|-----|
| Artifact manifest (agent+golden ver+sha256) | `handleSetArtifacts` (configs.go:664) | **Present** — form on `configs.html:52-71` | None. ✔ |
| Version floor — global | `SetGlobalMinControllerVersion` | **Present** — `configs.html:34-46` | None. ✔ |
| Version floor — per-customer | `handleSetCustomerFloor` (configs.go:697) | **Present** — `customer_unified.html:452-458` | None. ✔ |
| DR recipe (secret-free) | `dr_recipe.go`, `GetDRRecipe` | **Present** — section + JSON download, `customer_unified.html:586-613` | None; reflects the current secret-free model. ✔ |
| **Host enrollment / host identity / host status** | `hosts`/`guests`/`host_reports`, `ListHosts`, host stale/disk/fill checkers | **None** | **F-M1** — no host view exists. |
| **Agentic update state / last result** | floor auto-update (`MaybeAutoUpdate`), report data | Hint only ("will auto-update") | **F-M2** — no last-result surfacing. |
| Host ops (escrow / desired-state / jobs / recovery / re-enroll) | many `*/hosts/...` APIs | **None** (CLI/global-key) | **F-M3** — likely intentional; document. |

---

## 5. Open questions for the operator

1. **`docker-setup.sh`** (F-S1): remove the Option-1 block entirely, or keep it labelled "legacy
   (pre-Proxmox) — do not use"? Does **any** current customer still run the bare-Docker model? If none,
   remove.
2. **Inbound data-plane buttons** (F-S4): Trigger Update / Push Config / Pull Config / Show Diff /
   Geo-disable all make the hub connect *into* the box, which `01-topology-and-trust.md` forbids. Are
   these (a) to be **retired** in favour of agent-driven update + box-pulled config, (b) kept as a
   blessed, documented exception (and do they actually work in prod via the CF tunnel public URL?), or
   (c) already known-dead? This drives whether F-S4 is a delete or a doc change.
3. **Host visibility** (F-M1): is an operator-facing Hosts page wanted now, or is host health
   deliberately email-alert-only for the moment?
4. **Operator-console language** (F-P2): standardize the whole console on English (translate the Geo
   block), or keep the customer-facing-Hungarian / operator-English split inside this page?

---

## 6. Notes

- No secrets were copied into this doc. The live page renders retrieval passwords / API keys
  (`customer_unified.html` Credentials card); those are referenced by location only, never reproduced.
- Nothing here was changed in code. Implementing any of the above is a follow-up spec the operator
  prioritizes from §2.
