9167cf53af
gates / gates (push) Successful in 23s
The hub has written every customer e-mail in Hungarian whatever the box was set to. The box has published its language since controller v0.247.0; nothing read it. Now it does. Nothing an operator reads changes. The Hungarian mails are byte-identical, and that is a diff rather than a reading: 56 goldens per language captured from v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged after every sentence was routed through the new bundle. - internal/i18n: flat bundle, 79 keys, hu authoritative + hu fallback, ceiling 0. - customerMessages/severityLabels are DERIVED from the bundle, so a sentence is written in one place and all 40+ tests that read those maps still work. - Language order: last reported -> created-with -> hu. reports.language defaults to EMPTY, never hu: "never told us" is not "chose Hungarian". - message_customer on POST /api/v1/event, additive and optional forever, for the sentences the box composes and the hub cannot translate. - The bind page is per-language, and its `expired` state stays Hungarian: it is the state an unknown token lands in, so rendering a real English customer's token in English would make the LANGUAGE answer what the TEXT refuses to. Two defects found inside the release: - R-581: the newest report was picked by received_at, which has SECOND granularity, so same-second reports tied and the winner was arbitrary. Ordered by the autoincrement id now. GetCustomers() still has the shape - row open. - R-582: the English copy-guard stems, ported word for word from Hungarian, convicted 141 honest sentences. The English claim is a phrase with a modal. R-555 closed: the language allowlist entry is out of wire_contract_gate.py. hub_copy_gate.py follows the sentences into the bundle - without that it would have scanned four files that no longer hold any customer text and reported success. Three new decoys incl. an innocent control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
78 lines
4.5 KiB
Markdown
78 lines
4.5 KiB
Markdown
---
|
||
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. **From v0.118.0 the `customerMessages` half is a line in `internal/i18n/locales/hu.json`
|
||
(`mail.event.<type>`) AND its English twin** — the map is derived from the bundle, and the
|
||
missing-key gate is held at zero. Customer copy lives in the bundle; the operator's mails do not.
|
||
- Status logic: OK (report younger than `alerting.stale_threshold`), WARN (past the threshold or
|
||
`health=warn`), DOWN (past 2× the threshold or `health=fail`). The threshold is **configuration**
|
||
(`manifests/hub.yaml`; 45 m by operator ruling 2026-09-17, R-549), and the display
|
||
(`controllerStatus`, `hostStatus`) and both checkers read the same value — never hardcode it.
|
||
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.
|
||
|
||
<!--
|
||
Seam-wiring / template gates: the fourth inert seam, hub v0.70.1. The v0.70.0 ghost-delete was fully
|
||
implemented server-side and fully dead UI because the button sat inside the wrong gate.
|
||
|
||
Health check / block I/O: measured, R-117 spike §6.3
|
||
(documentation/audits/SPIKE-r117-bind-liveness-2026-07-30.md): a probe stayed in D state 3m50s after
|
||
kill -9; a buffered write with no fsync blocked too (O_CREAT needs journal access); and
|
||
statfs/getdents returned HEALTHY on a namespace that EIOs every byte — fast, and wrong.
|
||
|
||
This rule is duplicated in felhom-agent/.claude/rules/health-checks.md and
|
||
felhom-controller/.claude/rules/gates.md. That is deliberate: all three write health checks, and a
|
||
pointer to a sibling repo's rule file does not load.
|
||
-->
|