docs: REUSE.md introduced — reuse map (hub+website+scripts+manifests) + reuse_refs_check.py gate + consolidated cross-repo REPORT

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-03 09:40:39 +02:00
parent b0de6b34f6
commit d331eb26d1
7 changed files with 312 additions and 90 deletions
+3
View File
@@ -11,6 +11,7 @@ This repo (`felhom.eu`) contains:
- **Architecture docs** (`documentation/`) — the **authoritative design home for the whole Felhom system**: `architecture/01..05-*.md` (topology/trust, controller module map, host-agent, signing, hub), `proxmox-platform.md`, and `tests/phase{0,1-2,3,4}-findings.md`. Read these before designing.
See `README.md` for full architecture/DNS/email/SEO docs. See `TASK.md` for the current task (if any).
See `REUSE.md` before writing new code.
## The Felhom system (so the hub's role is in context)
@@ -54,6 +55,8 @@ The planning/architecture assistant ("project Claude", in claude.ai) writes spec
>
> **Never write secrets** — tokens, passwords, private keys, API keys — into `CHANGELOG.md`, `REPORT.md`, or any committed file. Reference them as "stored out-of-band" instead.
- Update `REUSE.md` if you added/changed/deprecated a shared helper or pattern (same commit).
## Tech stack (Hub)
- **Language:** Go 1.24+ (build server is go1.26.0).
+6
View File
@@ -0,0 +1,6 @@
# CONTEXT — felhom.eu working state
> Created with the REUSE.md rollout (2026-07-03). Authoritative history: `hub/CHANGELOG.md` (hub),
> `website/CHANGELOG.md`, `scripts/CHANGELOG.md`; end-of-task detail in `REPORT.md`.
- **2026-07-03 — `REUSE.md` exists at the repo root** (hub helpers / website conventions / gate scripts / manifest patterns, code-verified); maintenance rule active: update it in the same commit that changes a shared helper. `scripts/reuse_refs_check.py` machine-checks its citations across all four felhom repos.
+80 -90
View File
@@ -4,114 +4,104 @@
---
# REPORT — TASK-D4: Felhom Hub re-skin (design system v2, final surface) → hub v0.30.0
# REPORT — docs: REUSE.md introduced across all four felhom repos (consolidated)
**Date:** 2026-07-02 · **Deployed:** k3s felhom-system `deploy/hub` @ `felhom-hub:0.30.1` (0.30.0 + the badge no-wrap follow-up; rolled out)
**Date:** 2026-07-03 · **Class:** docs-only (no code change, no version bumps, no builds, no deploys)
**Deliverable:** a per-repo `REUSE.md` reuse map (canonical helpers / patterns / traps / seams /
extension points / known duplication), code-verified at file+symbol, plus `scripts/reuse_refs_check.py`
as the staleness gate, plus CLAUDE.md maintenance hooks.
## 1. Baselines
## 1. Baselines used (all matched the task's expected hashes; trees clean, pulled fresh)
- felhom.eu `main` @ `43189e8` (D3 final) — matched; main had not moved.
- felhom-controller @ `d0c70a7` — read-only woff2 source (byte-copied).
## 2. The enumerated OverallStatus set and its mapping
From source (server.go `handleDashboard`, configs.go `overallStatus`, hosts.go `hostStatusClass`):
**ok, warn, down, pending, disabled, blocked** (+ `fail` accepted as a statusColor input, `stale`
in the hosts domain). Complete mapping (exception-color principle):
| Status | Token | Reasoning |
| Repo | `main` @ baseline | State |
|---|---|---|
| ok | `nominal` | operating normally — blue, not green |
| warn | `warn` | degraded / stale report — amber |
| stale (hosts) | `warn` | consistent with hostStatusClass's existing warn reuse |
| down | `crit` | outage — red |
| fail | `crit` | health=fail — red |
| pending | `neutral` | a not-yet-provisioned customer is a NORMAL fleet state (Peti Debian stays quiet) |
| disabled | `neutral` | deliberately paused — not a deviation |
| blocked | `warn` | operator cut a customer off: intentional but attention-worthy — amber, not red (not an outage) |
| unknown/"" | `neutral` | unknown values stay quiet |
| felhom-controller | `d0c70a7da6f4060422a545cea2396f9df5f7d61f` | v0.98.3 |
| felhom-agent | `0db09bf34fe2eff4b80ac77ed19afa18aef7675f` | v0.61.0 |
| felhom.eu | `b0de6b34f61fef2e14807fba2c6176b888dc3327` | hub v0.30.1 |
| app-catalog-felhom.eu | `bb81cd414afcb89e28ac60f405133f40195e6c43` | n/a |
A healthy fleet dashboard is blue/neutral only. Event severity counts remain filled amber/red
badges (exceptions stay loud, per the design-system `.badge` addendum).
## 2. Files created/modified per repo
## 3. Commits
- **felhom-controller:** `REUSE.md` (new, 210 lines); `CLAUDE.md` (+2 hook lines); `CHANGELOG.md`
(docs entry); `CONTEXT.md` (note); `REPORT.md` (overwritten).
- **felhom-agent:** `REUSE.md` (new, 160 lines); `CLAUDE.md` (+2 hook lines); `CHANGELOG.md`;
`CONTEXT.md`; `REPORT.md`.
- **felhom.eu:** `REUSE.md` (new, 151 lines — hub + website + scripts + manifests);
`scripts/reuse_refs_check.py` (new gate); `CLAUDE.md` (+2 hook lines); `hub/CHANGELOG.md` +
`scripts/CHANGELOG.md` (docs entries); `CONTEXT.md` (new, minimal — repo had none); `REPORT.md`
(this file).
- **app-catalog-felhom.eu:** `REUSE.md` (new, 58 lines, light §2/§5 variant per spec); `CLAUDE.md`
(new, minimal — repo had none; full CLAUDE.md is a separate task); `CHANGELOG.md`; `CONTEXT.md`
(new, minimal); `REPORT.md`.
| Commit | Content |
## 3. Commits pushed to `main` (one per repo)
| Repo | Commit |
|---|---|
| `bc8d54d` | Part 1: fonts embedded + `/static/fonts/` route, 12-symbol sprite partial on all 9 pages, statusColor semantic remap, statusIcon retired, truth-table + parse tests |
| `c136308` | Part 2: style.css v2 (tokens, tag-style status badges + CSS dots, hairline rows, row tints warn/down only), two-tone H1 ×8, charts retinted, inline hexes swept, login retinted |
| `fc79a3a` | manifests: hub image 0.30.0 (GitOps truth kept in sync with the set-image deploy — repo convention; auto-sync is off) |
| (docs) | Part 3: hub/CHANGELOG v0.30.0 + this REPORT |
| felhom-controller | `eda33400da0fd6a3b80271c44df24a177f7a444a` |
| felhom-agent | `2a024b603973e79a0c84d6847a7cde0898091c61` |
| app-catalog-felhom.eu | `c5a3d1b15b71103a3d1da217f1ff5a834857375c` |
| felhom.eu | the commit carrying this REPORT (HEAD of `main` after this push) |
## 4. Tests + red-proof
## 4. Per-repo sweep counts (evidence of depth)
`go build ./... && go vet ./... && go test ./...` green after each commit (hub/).
New tests (the hub web package previously had none for funcmap/templates):
- `TestStatusColorTruthTable` — full enumerated set. **Red-proof:** against the stashed pre-change
implementation it failed exactly as required (`statusColor("ok") = "#4ade80", want "nominal"`,
warn→`#facc15`, stale→`#94a3b8`, down→`#f87171`). Restored → pass.
- `TestTemplatesParseWithFuncmap` — all 9 templates parse with the production funcmap.
| Repo | funcs scanned (non-test) | §1 rows | §3 traps | §6 duplications |
|---|---|---|---|---|
| felhom-controller | 1174 | 62 | 8 | 12 |
| felhom-agent | 702 | 48 | 10 | 7 |
| felhom.eu (hub Go) | 297 | 34 | 8 | 5 |
| app-catalog | 53 apps scanned | 0 (by design) | 3 | 6 |
## 5. Grep gates (before → after, hub/internal/web non-test files)
Trap sweeps ran mechanically per §Method (`--delete`, `wipefs`, `mkfs`, `os.RemoveAll`, `rm -rf`,
`sfdisk`, `dd `, `Format`, `Wipe`) in every repo; each hit classified as guarded-canonical (§1, guard
named) or §3 trap. Known traps (rsyncMirror `--delete`, gokapi entrypoint, AGENT-001 TOCTOU shape,
allowedEventTypes 400) were independently rediscovered from code.
`#0f172a` 2→0 · `#1e293b` 2→0 · `#334155` 3→0 · `#60a5fa` 3→0 · `#4ade80` 5→0 · `#facc15` 1→0 ·
`#f87171` 7→0 · `#94a3b8` 10→0 · `#64748b` 3→0 · `#475569` 3→0 · `#e2e8f0` 2→0 · `#0088cc`
(login) →0 · `statusIcon` template usage 2→0 · inline `style="color: {{statusColor}}"` 2→0 ·
box-shadow 0→0 (never had any).
## 6. API freeze proof
`git diff 43189e8..HEAD --stat` touches ONLY `hub/internal/web/**` (+ hub/CHANGELOG.md,
manifests/hub.yaml one-line tag, REPORT.md). Nothing under `hub/internal/api/`,
`hub/internal/store/`, or any ingestion/notify/monitor package.
## 7. Deployment + Scenario E (live E2E)
## 5. reuse_refs_check.py output (final, all four repos)
```
deployment "hub" successfully rolled out
hub-757d8dcb46-pq84j 1/1 Running 0 13s
[INFO] Listening on :8080 (felhom-hub 0.30.0)
OK [felhom-controller]: 65 cited paths, all exist
OK [felhom-agent]: 57 cited paths, all exist
OK [felhom.eu]: 30 cited paths, all exist
OK [app-catalog-felhom.eu]: 10 cited paths, all exist
```
Scenario E **passed**: hub deployed 22:40 CEST; the demo controller's next 15-minute cycle at
`2026/07/02 20:45:19 UTC (22:45 CEST)` logged `Hub report pushed successfully (5423 bytes)`
an HTTP 200 from the freshly deployed hub's `/api/v1/report`, proving ingestion end-to-end after
the re-skin (the controller only logs success on a 2xx). The dashboard LAST SEEN visual
confirmation joins the authenticated Chrome pass (§8).
## 8. Chrome checks (authenticated pass — operator logged in)
(The gate earned its keep during authoring: it caught the controller map citing
`scripts/{template_id_gate,emoji_gate}.py` — the real location is `controller/scripts/` — fixed
before commit.)
- **Dashboard (Scenario A/B):** two-tone "Felhom Hub", navy tokens, hairline rows. DOM-asserted:
**0 green** and **0 red** elements on the healthy fleet; OK = blue outline tag + dot
(`rgb(46,168,245)`); PENDING (Peti Debian) = quiet neutral tag with hollow dot; 0 radii beyond
0/2px/50%. (First load showed the OLD skin from the browser's 1-hour /style.css cache — a hard
reload fixed it; noted in Observations.)
- **Fonts (Scenario C):** `document.fonts.check` true for both faces; woff2 served from
`/static/fonts/`; **0 external requests**; "Demo Ügyfél" renders in Plus Jakarta Sans
(screenshots — dashboard + customer page H1).
- **Charts (Scenario D):** app_detail (nextcloud) memory trend — avg `#2EA8F5` filled, peak
`#8E7CE8` dashed, catalog-limit `#E0A93E` dashed; clearly distinguishable (screenshot).
Severity `warn` chips filled amber in Known Issues (exceptions loud).
- **Scenario E visual:** dashboard LAST SEEN "4 min ago" + Controller 0.98.3 in the version
column — the post-deploy report cycle rendered.
- Customers/customer_unified, Apps, Hosts (blue ONLINE tag, neutral NO REPORT), Configuration
(rendered read-only, nothing submitted; the artifact manifest shows agent 0.60.0 / golden
0.85.1 selected — unchanged) all render v2.
- **v0.30.1 follow-up:** the pass caught multi-word badges (PENDING / NO REPORT) wrapping between
the CSS dot and label in narrow columns — fixed with `white-space: nowrap` on `.status-badge`,
built + deployed (manifest bumped).
## 6. Observations — doc↔code drift & findings (recorded, NOT acted on)
## 9. NOT live-validated
**Worth operator attention:**
- **hub severity-coercion gotcha (NEW finding):** the remembered "dispatcher drops critical" issue is
FIXED in code (`severityNotifies` routes warning/error/critical since v0.24.0), **but**
`handleEvent` (hub/internal/api/handler.go) coerces any severity outside {info,warning,error} —
including `"critical"` — to `"info"` at ingest, so a controller-POSTed critical event never
notifies. Hub-internal events bypass this. Recorded in felhom.eu REUSE.md §3.
- **Committed secrets:** `manifests/felhom.secret.yaml` still carries inline `stringData`
credentials (healthchecks superuser pw + Django secret — not deployed; umami APP_SECRET +
POSTGRES_PASSWORD — deployed), unlike the de-gitted Resend key. Recorded as a §3 trap; rotation/
de-git is an operator follow-up.
- warn/crit fleet states (no customer is currently degraded) — truth-table-tested only.
- The Configuration page was NOT submitted/altered (per spec §12).
**Stale docs (each a separate refresh task):**
- `felhom-agent/CLAUDE.md` says "Current: v0.31.0" and omits ~8 live packages (localapi, signedjobs,
escrow, capability, guesthook, lanresolver, desired, provision).
- `felhom.eu/CLAUDE.md` "Hub — current state (v0.7.x)" is ~23 minor versions stale (live 0.30.1).
- `felhom-controller/CLAUDE.md` layout lists ~7 of ~30 internal packages; bare-metal deploy steps
superseded (already flagged in-file).
- app-catalog `README.md`: phantom `filebrowser/` template, missing `recipe-importer/`, field-type
table omits `subdomain`/`secret_input`, smtp_mapping list undercounts (2 listed, 6 real).
## 10. Observations (not acted on)
**Duplication highlights (full lists in each REUSE.md §6):** controller atomic-write ×4 (+1 inline,
two with deliberate non-atomic bind-mount fallback), dir-size ×6, JSON writers ×5/3 envelopes;
agent lsblk parsers ×2, atomic JSON store ×3, run() helper ×4, plus the acknowledged
antiRetarget↔WipeExecutor mirror; hub Resend sender ×2 + Hungarian formatter ×2 (frozen legacy
path) and semver-compare ×2 with different failure semantics; agent dead config knobs
`Binaries.MkfsExt4/MkfsXfs` (never executed — wrapper paths are hardcoded).
1. The hub login page is inline HTML in server.go (two copies, error + plain) — a template would
be cleaner; retinted only.
2. customer_unified.html carries a small per-page `<style>` block (toggle styles) — tokens updated
in place; consolidation into style.css is a candidate for a later pass.
3. `memoryColor`/`accuracyClass` (mem-ok/warn/danger) already returned classes; their CSS is now
v2 but the class names keep their legacy spelling (grep-safe rename candidate).
4. Session store is in-memory — every deploy logs the operator out; fine for a single-operator
tool, noted for the future.
## 7. Maintenance hooks now active
Every repo's CLAUDE.md: "See `REUSE.md` before writing new code" + "update REUSE.md in the SAME
commit that adds/changes/deprecates a shared helper or pattern". Staleness gate:
`python scripts/reuse_refs_check.py <repo-root> [...]` (run from felhom.eu). Reviewer-side: symbols
spot-verified at file:line (line numbers in the maps are landmarks only).
+151
View File
@@ -0,0 +1,151 @@
# REUSE.md — felhom.eu (hub + website + scripts)
> Before writing new code, check here. Canonical helpers, patterns to copy, traps to avoid.
> Maintenance: update in the SAME commit that adds/changes/deprecates a shared helper.
> Entries cite file + symbol. Line numbers are landmarks only — reconfirm before editing.
## 1. Canonical helpers (MUST reuse — do not reinvent)
### Report ingest & API auth (hub/internal/api/)
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `(*Handler).checkAuthCustomer` | hub/internal/api/handler.go (~L94) | `(r) (customerID string, isGlobal, ok bool)` | Bearer auth for controller-facing endpoints (global key OR per-customer key) | Global key → `("", true, true)`: caller must then trust body `customer_id`. Constant-time compare on global key. |
| `(*Handler).checkAuthHost` | hub/internal/api/handler.go (~L119) | `(r) (hostID, customerID string, isGlobal, ok bool)` | Bearer auth for agent-facing endpoints (global OR per-host key) | Sibling of checkAuthCustomer — do NOT mix the two token namespaces. Global key requires the host row to already exist (see handleHostReport). |
| `(*Handler).handleEvent` + `allowedEventTypes` | hub/internal/api/handler.go (~L1115 / ~L1063) | `POST /api/v1/event` | The ONLY controller→hub structured-event ingest | Unknown `event_type` → 400 (add to the map FIRST). Severity coerced to info/warning/error — `"critical"` silently becomes `"info"` (see §3). |
| `(*Handler).handleHostReport` | hub/internal/api/handler.go (~L464) | `POST /api/v1/host-report` | Agent heartbeat ingest: denorm + guest upsert | Body cap via LimitReader; per-host key enforces `host_id` match (403 on mismatch); `received_at` is the dead-man's-switch. |
| `(*Handler).handleConfigRetrieve` | hub/internal/api/handler.go (~L1484) | `GET /api/v1/config/{id}`, header `X-Retrieval-Password` | Canonical password-gated retrieval endpoint | Constant-time compare vs `cfg.RetrievalPassword`; 404-before-401 ordering. `handleArtifactManifest` mirrors it EXACTLY — keep them in lockstep. |
| `writeJSON` | hub/internal/api/dr.go (~L25) | `(w, code int, v any)` | JSON responses in api package | Only used in dr.go so far; prefer it over ad-hoc byte-writes for new endpoints. |
| `(*mailRateLimiter).allow` | hub/internal/api/mail.go (~L48) | `(key string) bool` | Per-key token-bucket rate limiting | Refill = perMinute/60 per sec, burst = perMinute; in-memory (lost on restart, accepted). `now` is injectable for tests. |
### Alerting / Resend / dispatcher (hub/internal/notify/, monitor/)
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `(*Dispatcher).ProcessEvent` | hub/internal/notify/dispatcher.go (~L67) | `(customerID, eventType, severity, message, detailsJSON, source)` | THE notification pipeline (operator + customer channels, cooldowns, prefs) | Call in a goroutine (handlers do `go d.ProcessEvent(...)`). No Resend key → silent no-op. `eventType=="test"` bypasses prefs/cooldown. |
| `severityNotifies` | hub/internal/notify/dispatcher.go (~L56) | `(severity string) bool` | Deciding whether a severity emails | warning/error/critical notify; info intentionally doesn't; anything else is logged as unrecognized (v0.24.0 fix — do not regress). |
| `FormatOperatorEmail` / `FormatCustomerEmail` | hub/internal/notify/templates.go (~L24 / ~L118) | `(...) (subject, body)` | Operator (English) / customer (Hungarian) email bodies | Customer messages come from the `customerMessages` map — add the Hungarian text when adding an event type. Budapest TZ via package `init()`. |
| `monitor.EventNotifyFunc` | hub/internal/monitor/staleness.go (~L14) | `func(customerID, eventType, severity, message, detailsJSON, source)` | Decoupling checkers from notify; wired to `dispatcher.ProcessEvent` in main | May be nil — always nil-check before calling (all checkers do). |
| `(*Store).LogNotification` | hub/internal/store/store.go (~L433) | `(customerID, eventType, severity, message, status, errorMsg, channel)` | Audit trail of every send attempt (sent/failed, per channel) | Log BOTH success and failure (dispatcher does). |
### App-mail passthrough (hub/internal/mailrelay/)
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `mailrelay.Sender` / `(*ResendSMTP).Send` | hub/internal/mailrelay/relay.go (~L24 / ~L50) | `Send(ctx, raw []byte, mailFrom, rcptTo) error` | Raw-MIME passthrough to Resend SMTP | Deliberately separate from notify's HTTP path — parse-then-API drops inline CID images (spike-proven). Do NOT "unify" them. Delivery verdict lands at DATA-close. |
| `mailrelay.FromDomain` | hub/internal/mailrelay/relay.go (~L130) | `(raw []byte) (string, error)` | From-HEADER domain extraction for allowlisting | Header domain, not envelope — Resend checks the header. |
### Web auth / session / CSRF (hub/internal/web/)
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `(*Server).RequireAuth` | hub/internal/web/server.go (~L359) | `(next http.Handler) http.Handler` | Session-cookie OR Basic-auth gate for all web routes | Empty passwordHash disables auth entirely (dev mode). Browsers → /login redirect; JSON-ish requests → 401. |
| `(*Server).validateCSRF` | hub/internal/web/server.go (~L446) | `(r) bool` | CSRF check — enforced centrally in `web.ServeHTTP` for every non-GET | No session cookie → returns true (Basic-auth path is exempt). New POST routes get CSRF for free; forms MUST embed `csrfField`. |
| `(*Server).csrfField` | hub/internal/web/server.go (~L483) | `(r) template.HTML` | Hidden `_csrf` input for HTML forms | Pass into template data on every form-rendering handler. |
| `(*Server).CleanupSessions` | hub/internal/web/server.go (~L110) | `(ctx)` — goroutine | Expired-session sweeper | Started once from main; 15-min tick. |
### Status tokens & template funcmap (hub/internal/web/)
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| funcMap in `web.New` | hub/internal/web/server.go (~L67) | `template.FuncMap{...}` | ALL template helpers (`timeAgo`, `statusColor`, `json`, `hubVersion`…) | Add new template funcs HERE, nowhere else. Tested by hub/internal/web/funcmap_test.go + render_test.go. |
| `timeAgo` | hub/internal/web/server.go (~L603) | `(t time.Time) string` | Human-relative timestamps in UI | — |
| `statusColor` | hub/internal/web/server.go (~L630) | `(status string) string` | Status → design-system-v2 token (nominal/warn/crit/neutral) | Class SUFFIX only, never inline color (D4). Exception-color principle: healthy = blue/neutral. |
| `(*Server).hostStatus` + `hostStatusClass`/`hostStatusLabel` | hub/internal/web/hosts.go (~L16/34/48) | `(lastReport *time.Time) string` | Host liveness badge | Uses the SAME threshold as HostStalenessChecker (down = 2× stale) — never invent a second definition. |
| `parseSQLiteTime` | hub/internal/store/store.go (~L1160) | `(s string) time.Time` | Parsing ANY timestamp read from SQLite | modernc/sqlite returns multiple formats; raw `time.Parse` will intermittently zero out. Always use this. |
| `compareVersions` | hub/internal/web/server.go (~L571) | `(a, b string) int` | X.Y.Z comparisons in web (floor checks, update-available) | Returns 0 on parse error — unparseable compares as "equal" (see §3). |
### Artifact manifest / Day-0 trust root
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `(*Store).GetArtifactManifest` / `SetArtifactManifest` | hub/internal/store/store.go (~L933 / ~L944) | `() ArtifactManifest` / `(m) error` | The DB-backed (hub_settings) Day-0 artifact record | This is the checksum TRUST ROOT the host-bootstrap verifies against — distinct from Gitea, which only stores bytes. |
| `(*Server).handleSetArtifacts` + `resolveArtifactSHA` | hub/internal/web/configs.go (~L644 / ~L680) | `POST /configuration/artifacts` | Operator UI to vouch artifact versions | With a Gitea client the sha is fetched AUTHORITATIVELY (submitted sha ignored); fetch failure refuses the save. Manual sha only in the no-creds fallback. |
| `(*gitea.Client).ListVersions` / `FileSHA256` | hub/internal/gitea/gitea.go (~L47 / ~L72) | `(ctx, pkg) ([]string, error)` / `(ctx, pkg, ver, file)` | Read-only Gitea generic-package metadata | sha comes from package metadata — artifact bytes are never downloaded. Newest-semver-first sort. |
| `(*Server).artifactChoices` | hub/internal/web/server.go (~L155) | `(ctx, pkg, file) []artifactChoice` | Version+sha dropdown data | nil Gitea client / unreachable → nil → UI degrades to manual entry. One bad version drops itself, not the list. |
| `(*Handler).handleArtifactManifest` | hub/internal/api/handler.go (~L1550) | `GET /api/v1/artifacts/{id}` | Serving the vouched set to the bootstrap script | Auth mirrors handleConfigRetrieve exactly. Unset manifest = empty fields, not an error. |
| `normalizeFloorInput` / `normalizeSHA256` | hub/internal/web/configs.go (~L27 / ~L627) | `(raw string) (string, bool)` | Validating operator-typed versions / shas | Empty string is VALID (means "clear"). Reuse for any new version/sha form field. |
### Config generation & secrets hygiene (hub/internal/configgen/)
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `configgen.Generate` | hub/internal/configgen/configgen.go (~L16) | `(templateYAML, cfg) (string, error)` | Producing a customer controller.yaml | Programmatic overrides (customer id/hub url/api_key) ALWAYS win over config_json; fresh session secret per generation. |
| `configgen.RandomHex` | hub/internal/configgen/configgen.go (~L110) | `(n int) (string, error)` | crypto/rand hex tokens (API keys, session secrets) | — |
| `configgen.RandomPassphrase` | hub/internal/configgen/passphrase.go (~L35) | `(wordCount int) (string, error)` | Human-dictatable Hungarian passphrases (retrieval passwords) | ~29K-word embedded list; 5 words ≈ 74 bits. |
| `(*Store).EffectiveMinControllerVersion` | hub/internal/store/store.go (~L960) | `(customerID) string` | Resolving the floor that actually applies (per-customer → global) | "" = no floor (Phase 2 inert). |
### Assets, store misc, scheduling
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `assets.Manager` (`New`, `ServeFile`, `ReSeed`) | hub/internal/assets/assets.go (~L44/190/122) | seed-dir → PVC sync + manifest | Serving app logos/screenshots to controllers | `ServeFile` sanitizes to `filepath.Base` (no traversal). `isAssetFile` enforces the naming convention. |
| `copyFile` | hub/internal/assets/assets.go (~L259) | `(src, dst) error` | THE atomic file write (tmp + rename) in the hub | Copy this shape for any new on-disk write. |
| `fileSHA256` | hub/internal/assets/assets.go (~L244) | `(path) (string, error)` | Streaming sha256 of a file | — |
| `(*Store).SaveEvent` | hub/internal/store/store.go (~L1003) | `(...) (int64, error)` | Persisting ANY event (controller or hub source) | Pair with dispatcher/`onEvent` — saving alone never notifies. |
| `store.GuestID` | hub/internal/store/store.go (~L1268) | `(hostID string, vmid int) string` | Canonical guest primary key | Never hand-concatenate host+vmid. |
| `scheduleDaily` | hub/cmd/hub/main.go (~L449) | `(ctx, name, "HH:MM", fn, logger)` | Daily jobs in Europe/Budapest (prune etc.) | Blocking — run as goroutine. `parseHM` returns 0,0 (midnight) on bad input. |
## 2. Canonical patterns (copy structure from THE named file)
| Pattern | Canonical file | Key traits |
|---|---|---|
| Monitor checker | hub/internal/monitor/staleness.go | Seed state on construction WITHOUT emitting events; in-memory `states` map under mutex; periodic `Check()`; `emitTransition` = SaveEvent then nil-checked `onEvent`; cleanup of vanished IDs. HostStaleness/HostDisk/HostLeaf/HostCapability/StorageFill all follow it. |
| API endpoint | hub/internal/api/handler.go `ServeHTTP` (~L139) + any handler | Path switch in ServeHTTP; first line of every handler = checkAuth{Customer,Host}; `io.LimitReader` body cap; typed anonymous payload struct; explicit 4xx strings. |
| Web POST action | hub/internal/web/configs.go `handleSetGlobalFloor` (~L602) | CSRF enforced centrally in web ServeHTTP; validate via `normalize*` helper; POST-redirect-GET with `?flash=` token; log INFO on success. |
| Optional dependency injection | hub/internal/web/server.go `Set*` setters (~L131148) / api handler `SetDispatcher` etc. | Constructor takes hard deps; optional subsystems wired via `SetX` after construction; nil = graceful degradation (never panic). |
| Seam-injected sender for tests | hub/internal/notify/dispatcher.go `sendEmailFn` (~L33) | Function-field defaulting to the real impl in the constructor; tests overwrite it. Same idea: `mailRateLimiter.now`, `mailrelay.Sender` fake. |
| Website page | website/index.html | UTF-8 **with BOM**; shared `<nav>`/`<footer>` byte-identical across pages (only `class="active"` differs); two-tone H1 = `<h1>…<span>accent</span></h1>`; all styling in website/assets/site.css tokens (`:root`) — zero embedded `<style>`; `?v=N` cache-bust on site.css/icons.svg; umami snippet; no CDN fonts; no emoji (sprite icons.svg instead). |
| Gate script | scripts/site_gates.py | Byte-level mechanical gates (BOM, emoji codepoint ranges, nav/footer diff, analytics, banned tokens, cache-bust); run `python scripts/site_gates.py` after ANY website change; non-zero exit on failure. |
| Fetch-validate-install (shell) | scripts/felhom-host-install.sh `step_agent_install` (~L1108) | `fetch_raw` to mktemp → syntax-check (`bash -n`) → `install -m0755 -o root -g root` → only then activate; guarded-mkfs wrapper installed BEFORE the sudoers that references it (ordering is the safety property). All mutations through `run()` (dry-run aware). |
| Website deploy (manifest) | manifests/webpage.yaml | git-sync sidecar (sparse-checkout `/website/` + `/scripts/`, `--link=current`) + init container waits for first sync; nginx serves `current/website`; push to main = deployed, no image build. |
| Secret handling (manifest) | manifests/hub.yaml (env, ~L142) | Secrets via `secretKeyRef` to OUT-OF-BAND secrets (`resend-api`, `gitea-creds`) created per documentation/runbooks/secrets.md — never inline stringData (see §3). |
| Hub deploy (GitOps) | manifests/hub.yaml `image:` (~L129) | Pinned explicit tag, bumped in git, deliberate ArgoCD sync (auto-sync OFF). Code push alone deploys nothing. |
## 3. Dangerous lookalikes — do NOT reuse
| Trap | Why it bites | Use instead |
|---|---|---|
| `(*Handler).handleNotify` + `formatNotificationEmail` + `sendResendEmail` (hub/internal/api/handler.go ~L1289/1624/1589) | Legacy pre-dispatcher notification trio: no cooldowns, no operator channel, no allowedEventTypes gate, duplicate Hungarian formatter. Controller path is FROZEN until slice-10 cutover. | `POST /api/v1/event``Dispatcher.ProcessEvent` + `notify.Format*Email` |
| Severity `"critical"` via `POST /api/v1/event` | `handleEvent` (~L1157) coerces unknown severities — including `critical` — to `"info"`, which never notifies. Silent alert loss. | Send `warning`/`error` from controllers, or extend the handleEvent switch AND allowedEventTypes together |
| `compareVersions` for anything security-ish (hub/internal/web/server.go ~L571) | Returns 0 (equal) on unparseable input — a garbage version passes a floor check. `gitea.compareSemver` behaves differently (lexical fallback). | Validate input with `normalizeFloorInput` first; then compareVersions is safe |
| Inline `stringData` secrets à la manifests/felhom.secret.yaml | Commits real credentials to git (healthchecks superuser pw, umami APP_SECRET/POSTGRES_PASSWORD still live there). | Out-of-band `kubectl create secret` + `secretKeyRef` (hub.yaml resend-api pattern; runbook documentation/runbooks/secrets.md) |
| `kubectl apply` / `kubectl set image` on manifests/ | ArgoCD app `felhom` reverts drift on next sync; live state lies about git. | Edit manifest in git → push → ArgoCD sync (CLAUDE.md steps 35) |
| `:latest` image tag in manifests | Re-push doesn't change the manifest → no redeploy; Synced/Rollback misreport. | Pinned version tag, bumped per deploy |
| grep/regex hunting emoji in website HTML | Windows grep false-negatives multibyte emoji (proven in D0). | `python scripts/site_gates.py` (codepoint-range check) |
| Adding a website page without touching site_gates.py | `PAGES` list (scripts/site_gates.py ~L22) is explicit — an unlisted page is silently ungated (BOM/nav/emoji drift undetected). | Add the filename to `PAGES` in the same commit |
## 4. Seams & interfaces (testing + cross-repo)
| Interface | Defined in | Implemented by | Fakes/tests at |
|---|---|---|---|
| `mailrelay.Sender` | hub/internal/mailrelay/relay.go (~L24) | `ResendSMTP` (prod) | fake sender in hub/internal/api/mail_test.go; hub/internal/mailrelay/relay_test.go |
| `Dispatcher.sendEmailFn` (func seam) | hub/internal/notify/dispatcher.go (~L33) | `(*Dispatcher).sendEmail` (Resend HTTP) | hub/internal/notify/dispatcher_test.go |
| `monitor.EventNotifyFunc` | hub/internal/monitor/staleness.go (~L14) | closure over `Dispatcher.ProcessEvent` (main.go) | hub/internal/monitor tests (captured-events func) |
| `api.ConfigTemplateProvider` | hub/internal/api/handler.go (~L24) | `web.TemplateFetcher` (Gitea-pulled controller.yaml template) | stub providers in api tests |
| `api.LatestVersionProvider` | hub/internal/api/handler.go (~L31) | `web.VersionChecker` (registry poll) | hub/internal/api/config_version_ack_test.go |
| `mailRateLimiter.now` (func seam) | hub/internal/api/mail.go (~L27) | `time.Now` | hub/internal/api/mail_test.go clock injection |
| Cross-repo: controller → hub | `POST /api/v1/report` (frozen) + `POST /api/v1/event` | felhom-controller repo | new event types MUST enter `allowedEventTypes` (hub/internal/api/handler.go ~L1063) or the controller gets 400 |
| Cross-repo: agent → hub | `POST /api/v1/host-report`, `/host-enroll`, jobs/desired-state/escrow routes (handler.go ~L145189) | felhom-agent repo | hub/internal/api/host_test.go, desired_test.go, escrow_test.go, dr_test.go |
| Cross-repo: Day-0 bootstrap → hub | `GET /api/v1/config/{id}` + `/artifacts/{id}` (X-Retrieval-Password) | scripts/felhom-host-install.sh (fetches + sha256-verifies against the hub-vouched manifest) | hub/internal/api/artifact_test.go |
| Cross-repo: controller ← hub assets | `GET /api/v1/assets/manifest` + `/assets/file/{name}` | felhom-controller pulls app logos/screenshots | assets manifest sha-based change detection |
## 5. Extension points (where new features plug in)
- **New event type**: add to `allowedEventTypes` (hub/internal/api/handler.go ~L1063) **and** `customerMessages` (hub/internal/notify/templates.go) **and** the customer-prefs default list if customer-notifiable. Missing the first = controller POST 400s (the known gotcha).
- **New monitor checker**: copy hub/internal/monitor/staleness.go (§2 pattern); wire in hub/cmd/hub/main.go with an `EventNotifyFunc`; severity must be warning/error/critical to notify.
- **New API route**: switch in `api.ServeHTTP` (handler.go ~L139); auth helper first line.
- **New web page/action**: switch in `web.ServeHTTP` (server.go ~L182) — non-GET gets CSRF automatically; template into hub/internal/web/templates/ (embedded FS, parsed in `web.New`); new helpers into the funcMap (server.go ~L67).
- **New template func**: funcMap in web.New only; add a case to hub/internal/web/funcmap_test.go.
- **New daily job**: `scheduleDaily` in hub/cmd/hub/main.go + add pruning to `pruneAll` if data grows.
- **New site gate**: append to scripts/site_gates.py; new website pages go into its `PAGES` list.
- **New artifact kind (Day-0)**: consts `pkg*`/`file*` (hub/internal/web/server.go ~L27), `ArtifactManifest` fields + settings keys (hub/internal/store/store.go ~L905), `handleSetArtifacts`, `artifactManifestResponse` (handler.go), and the install script's verify step.
- **New host-install step**: `step_*` function in scripts/felhom-host-install.sh using `run()`/`fetch_raw`/`die` helpers; keep dry-run coverage.
## 6. Known duplication (observed — NOT fixed)
- Resend HTTP sender ×2: `(*Handler).sendResendEmail` (hub/internal/api/handler.go ~L1589) ≈ `(*Dispatcher).sendEmail` (hub/internal/notify/dispatcher.go ~L185) — byte-near-identical POST to api.resend.com. Kept because the handler copy serves the frozen legacy /notify path.
- Hungarian customer-email formatter ×2: `formatNotificationEmail` (hub/internal/api/handler.go ~L1624) vs `notify.FormatCustomerEmail` (hub/internal/notify/templates.go ~L118). Legacy vs dispatcher; the legacy one lacks the per-event-type message map.
- Semver compare ×2 with DIFFERENT fallback semantics: `web.compareVersions` (hub/internal/web/server.go ~L571, parse error → 0) vs `gitea.compareSemver` (hub/internal/gitea/gitea.go ~L115, parse error → lexical). Documented as deliberate (import-cycle avoidance) in gitea.go, but the behavior drift is not.
- Checker-family structural repetition: staleness.go vs host_staleness.go, and host_disk.go vs storage_fill.go (band/bandRank/bandLabel vs bandForPercent) — same skeleton re-implemented per domain; treated as the accepted §2 pattern rather than a defect.
- Duration formatting ×2: `monitor.formatDuration` (hub/internal/monitor/staleness.go ~L187) vs `web.timeAgo` (hub/internal/web/server.go ~L603) — different audiences (email vs UI) but overlapping logic.
+10
View File
@@ -1,5 +1,15 @@
# Felhom Hub — Changelog
## docs — REUSE.md introduced (2026-07-03)
Cross-repo reuse-map rollout (docs-only, no code change, no version bump). New `REUSE.md` at the
repo root covering hub + website + scripts + manifests: canonical helpers (34 rows), patterns
(monitor checker, website page, gate script, GitOps deploy), dangerous lookalikes (legacy /notify
trio, severity-`critical` coercion at handleEvent ingest, inline stringData secrets, kubectl-apply
drift…), seams, extension points, and observed duplication (5 clusters, NOT fixed). New
`scripts/reuse_refs_check.py` machine-checks every cited path in all four repos' REUSE.md files.
CLAUDE.md gains the REUSE.md pointer + same-commit maintenance rule.
## v0.30.1 — status badge no-wrap (2026-07-02)
Found in the authenticated D4 validation pass: multi-word status tags (PENDING in a narrow
+8
View File
@@ -1,5 +1,13 @@
# Felhom scripts — Changelog
## reuse_refs_check.py — new gate: REUSE.md citation checker (2026-07-03)
Staleness defense for the new per-repo `REUSE.md` reuse maps. Takes repo roots as argv, extracts
every cited `*.go/*.py/*.html/*.css/*.yml/*.yaml/*.sh` path (slash-containing tokens only — bare
filenames are conventions, not citations), verifies each exists; prints offenders, non-zero exit on
any missing path. Symbols are spot-verified by the reviewer, not this script.
Usage: `python scripts/reuse_refs_check.py <repo-root> [...]`.
## felhom-host-install.sh v1.8.0 — install the guarded-mkfs wrapper (Impl-1 Part B) (2026-07-01)
Companion to felhom-agent v0.54.0 (format-safety foundation). During agent install, fetch + install the
+54
View File
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""REUSE.md refs check — staleness defense for the per-repo reuse maps.
Usage: python scripts/reuse_refs_check.py <repo-root> [<repo-root> ...]
For each repo root given, parses <root>/REUSE.md, extracts every cited file path
(*.go, *.py, *.html, *.css, *.yml, *.yaml, *.sh) and verifies the file exists in
the tree. Only slash-containing (repo-relative) tokens are checked bare filenames
are conventions, not citations. Prints offending lines; exits non-zero if any cited
path is missing. Symbols are NOT checked here those are spot-verified by the
reviewer at file:line.
"""
import io, os, re, sys
# path-looking tokens ending in a checked extension; globs (*) are conventions, not refs
PATH_RE = re.compile(r'[A-Za-z0-9_][A-Za-z0-9_./\-]*/[A-Za-z0-9_./\-]*\.(?:go|py|html|css|yml|yaml|sh)\b')
fails = 0
def check_repo(root):
global fails
root = os.path.abspath(root)
reuse = os.path.join(root, "REUSE.md")
name = os.path.basename(root)
if not os.path.isfile(reuse):
print("FAIL [%s]: no REUSE.md at %s" % (name, reuse))
fails += 1
return
seen, missing = set(), 0
with io.open(reuse, encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
for m in PATH_RE.finditer(line):
p = m.group(0)
if "*" in line[max(0, m.start() - 2):m.end() + 2]:
continue # glob like scripts/*.py — a convention, not a file ref
if p in seen:
continue
seen.add(p)
if not os.path.isfile(os.path.join(root, p)):
print("FAIL [%s] line %d: cited path missing: %s" % (name, lineno, p))
missing += 1
if missing:
fails += missing
else:
print("OK [%s]: %d cited paths, all exist" % (name, len(seen)))
if len(sys.argv) < 2:
print(__doc__)
sys.exit(2)
for r in sys.argv[1:]:
check_repo(r)
sys.exit(1 if fails else 0)