Files
felhom.eu/.claude/rules/hub.md
T
admin 9167cf53af
gates / gates (push) Successful in 23s
hub v0.118.0: the household's e-mails follow the household's language (R-558 Part A)
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
2026-09-18 16:20:11 +02:00

78 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.
-->