--- paths: ["hub/**"] --- # Hub — architecture, constraints, deploy ## What the hub is Operator backend. Authors operator *intent*, mirrors box *reality*, holds **no data-plane role**, and **never connects inbound to a box**. If it dies, apps keep serving; only management degrades. Three inbound contracts, and one is frozen: the agent's host-domain report (`POST /api/v1/host-report`, the heartbeat/dead-man's-switch), the legacy controller report (`POST /api/v1/report`, **frozen until the slice-10 cutover — do not modify**), and structured controller events (`POST /api/v1/event`, gated by `allowedEventTypes`). Everything else — checkers, the two-tier notification dispatcher, the app-mail relay, customer-config and Day-0 artifact-manifest management, the operator UI — is mapped in **`REUSE.md`**. Full design: `documentation/architecture/05-hub-architecture.md`. ## The two stack constraints The dependency list is `hub/go.mod`'s business and the deploy shape is `manifests/`. Only these two cannot be read off the code: - **No web frameworks.** Go stdlib `net/http` + `html/template`, and it stays that way. - **Secrets via out-of-band `secretKeyRef` — never inline `stringData`** (`REUSE.md` §3). ## Deploy — GitOps, and the manifest is the truth **Full runbook: the `felhom-build-deploy` skill.** The load-bearing rules: - **A code change + CHANGELOG bump deploys NOTHING.** The running image changes only when `manifests/hub.yaml`'s `image:` tag changes in git and the app is synced. - **Pin explicit versions, never `:latest`. Never bare `kubectl set image` / `kubectl apply`** — reverted on the next sync. - **The live image can lag the CHANGELOG** when a bump was committed but the manifest/sync step never happened — reconcile via the manifest, not the changelog. - Green gate before any hub commit: `go build ./... && go vet ./... && go test ./...` in `hub/`. ## Rules that bite here - **Seam-wiring — it covers TEMPLATE GATES:** a feature is not shipped until its entry point is reachable. Any conditional affordance (`{{if .Flag}}` around a button, form or script) ships with a render test **per branch of the gate**. Handler tests that POST directly prove nothing about reachability. - **A health check issues no block I/O.** A probe that touches a wedged device enters uninterruptible sleep, survives `SIGKILL`, and cannot be recovered until the device returns or the host reboots — so `systemctl restart` hangs too. A timeout protects the caller's control flow and nothing else: the blocked thread remains. Liveness is decided from `/proc` and kernel state, never by reading or writing the filesystem. - New event types must enter `allowedEventTypes` **and** `customerMessages` together, or `POST /event` 400s. - Status logic: OK (report < 30m), WARN (30m–1h or `health=warn`), DOWN (> 1h or `health=fail`). Host-liveness thresholds are **shared** between UI and checker — never invent a second definition. - SQLite timestamps vary in format — always `parseSQLiteTime()`. - **Logging**: DEBUG = flow detail, INFO = state change + duration; operator English; keys never values (`documentation/runbooks/logging-conventions.md`). The bundle secret-gate fails closed.