docs+skills: felhom-{build-deploy,ui-design,testing} skills + install_skills.py (junction); CLAUDE.md refresh (version-free); consolidated 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 11:27:48 +02:00
parent ad61e96abd
commit 9282d60f96
9 changed files with 502 additions and 137 deletions
+73 -58
View File
@@ -1,14 +1,22 @@
# CLAUDE.md — Project Instructions for Claude Code (`felhom.eu`)
> Read automatically by Claude Code when it works in this repo. Keep it updated as the project evolves. Cross-repo orientation (the felhom system, artifact taxonomy, access) lives in the workspace-root `e:\git\CLAUDE.md`; this file is `felhom.eu`-specific.
> Read automatically when Claude Code works in this repo. Stable orientation only — **current state
> lives in `CONTEXT.md` and the tops of `hub/CHANGELOG.md` / `scripts/CHANGELOG.md` /
> `website/CHANGELOG.md`**, never here. Cross-repo orientation (the felhom system, artifact
> taxonomy, access): workspace-root `e:\git\CLAUDE.md`; this file is `felhom.eu`-specific.
## Project overview
This repo (`felhom.eu`) contains:
This repo contains:
- **Website** (`website/`) — static HTML at felhom.eu, served via k3s nginx + git-sync sidecar.
- **Hub** (`hub/`) — Go application (felhom-hub) the **operator backend**, on k3s at `hub.felhom.eu`.
- **Hub** (`hub/`) — Go application (felhom-hub), the **operator backend**, on k3s at `hub.felhom.eu`.
- **K8s manifests** (`manifests/`) — k3s deployment manifests for felhom-system services.
- **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.
- **Architecture docs** (`documentation/`) — the **authoritative design home for the whole Felhom
system**: `architecture/01..05-*.md`, `proxmox-platform.md`, `tests/phase*-findings.md`,
runbooks, audits. Read these before designing.
- **Skills** (`skills/`) — the versioned source of the Claude Code skills
(`felhom-build-deploy`, `felhom-ui-design`, `felhom-testing`); install/update with
`python scripts/install_skills.py` (junction into `~/.claude/skills/`).
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.
@@ -16,18 +24,22 @@ See `REUSE.md` before writing new code.
## The Felhom system (so the hub's role is in context)
Felhom is **Proxmox-based**, with a locked **three-component model**:
- **Hub** (this repo, `hub/`) — operator backend. Authors operator *intent*; mirrors box *reality*; holds **no data-plane role** and never connects inbound to a box.
- **Hub** (this repo, `hub/`) — operator backend. Authors operator *intent*; mirrors box *reality*;
holds **no data-plane role** and never connects inbound to a box.
- **Host agent** (repo `felhom-agent/`) — one per Proxmox host; owns all Proxmox interaction.
- **In-guest controller** (repo `felhom-controller/`) — one per customer LXC; Docker-only.
The hub is **not** just controller monitoring anymore. As of slice 3 it ingests **two report streams**: the agent's host-domain report (`POST /api/v1/host-report`, the heartbeat) and the legacy controller report (`POST /api/v1/report`). The controller path is **frozen and retires at the slice-10 cutover** — do not modify it until then.
## Hub — architecture (version-free; current version = `manifests/hub.yaml` image tag)
## Hub — current state (v0.7.x)
- **Tables:** `customer_configs`, `events`, `app_telemetry`/`app_log_issues`, the legacy `reports`, and the slice-3 host-domain additions `hosts` / `guests` / `host_reports` (additive; columns marked inert exist for the slice-10 cutover but are unused now).
- **Auth:** Bearer — global key, per-customer key (legacy), and per-host key (`GetHostByAPIKey`, slice 3). Provisional global-key host mint at `POST /api/v1/admin/hosts`.
- **Monitoring:** the controller `StalenessChecker` (over `reports`) AND a sibling `HostStalenessChecker` (over `host_reports`, emitting `host_stale`/`host_down`/`host_recovered`).
- Two-tier notifications (operator English / customer Hungarian, Resend, cooldowns); `events` audit.
The hub ingests two report streams — the agent's host-domain report (`POST /api/v1/host-report`, the
heartbeat/dead-man's-switch) and the legacy controller report (`POST /api/v1/report`, **frozen until
the slice-10 cutover — do not modify**) — plus structured controller events (`POST /api/v1/event`,
gated by `allowedEventTypes`). Around them: staleness/disk/storage-fill/leaf/capability monitor
checkers, the two-tier notification dispatcher (operator English / customer Hungarian, Resend,
cooldowns), the app-mail relay, customer-config + Day-0 artifact-manifest management (the checksum
trust root the host bootstrap verifies against), assets serving, and the password-gated operator web
UI. Package map, helpers, seams, extension points: **`REUSE.md`** (e.g. new event types must enter
`allowedEventTypes` + `customerMessages` together).
## Code quality rules
@@ -35,80 +47,83 @@ The hub is **not** just controller monitoring anymore. As of slice 3 it ingests
- Handle edge cases without overcomplicating.
- Add debug capabilities (logging, verbose output).
- If you need more input or troubleshooting output, **ask first — don't guess**.
- Testing doctrine (non-hollow tests, red-proofs, seams): use the `felhom-testing` skill.
- UI/design work (tokens, gates, copy rules): use the `felhom-ui-design` skill.
## Workflow & artifacts
The planning/architecture assistant ("project Claude", in claude.ai) writes specs and validates pushes; **you (Claude Code) implement**. A file being open in the editor is NOT an instruction.
The planning/architecture assistant ("project Claude", in claude.ai) writes specs and validates
pushes; **you (Claude Code) implement**. A file being open in the editor is NOT an instruction.
- **`TASK.md` / `TASK-*.md`** — a spec for you to implement. Then push and update this repo's changelog (`hub/CHANGELOG.md`) and root `REPORT.md` per the convention below.
- **`RUNBOOK-*.md`** — an operational procedure. CC executes the steps it has access and capability for, including live validation on the demo nodes and the demo Proxmox host (CC has root@felhom-pve SSH + the felhom-agent token). A step is human-only only when it genuinely needs physical presence, a real-world decision, or credentials CC truly lacks — mark those steps HUMAN. Do not decline a whole procedure because it touches a live host or a privileged token. (Judgment still applies: confirm before irreversible ops on real customer data — but demo scratch guests are fair game.)
- **`TASK.md` / `TASK-*.md`** — a spec for you to implement. Then push and update `hub/CHANGELOG.md`
and root `REPORT.md` per the convention below.
- **`RUNBOOK-*.md`** — an operational procedure. CC executes the steps it has access and capability
for, including live validation on the demo nodes and the demo Proxmox host (CC has root@felhom-pve
SSH + the felhom-agent token). Mark a step HUMAN only when it genuinely needs physical presence, a
real-world decision, or credentials CC truly lacks.
- Validation of a push against a spec's criteria is project Claude's job, not yours, unless asked.
- **Browser automation available** (`claude-in-chrome` bridge installed): CC can drive the hub
dashboard (`hub.felhom.eu`), the website, or any `*.demo-felhom.eu` UI in a real browser for live
validation + screenshots. Caveat: the browser tools attach only to a session STARTED AFTER the bridge
connected — if missing, open a fresh task (don't assume it's broken); `/mcp` should show
`claude-in-chrome · connected`, else `/chrome` → reconnect.
- **Browser automation available** (`claude-in-chrome`): CC can drive `hub.felhom.eu`, the website,
or any `*.demo-felhom.eu` UI for live validation + screenshots. Caveat: tools attach only to a
session STARTED AFTER the bridge connected. Note: the hub UI is operator-password-gated — CC
cannot log in; verify UI changes via render tests instead.
> **In every repository where you make a change, update both files in that repo:**
> - **`CHANGELOG.md`** — a cumulative log of **all** changes; newest entry on top.
> - **`REPORT.md`** — **overwrite** with a summary of the **most recent** implementation (or significant validation/operational run) only; not cumulative.
> - **`CHANGELOG.md`** — cumulative log, newest on top (here: per-area `hub/`, `scripts/`, `website/`).
> - **`REPORT.md`** — **overwrite** with the most recent implementation/validation summary only.
>
> **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.
> **Never write secrets** into any committed file — reference them as "stored out-of-band".
- 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).
- **Web:** stdlib `net/http` + `html/template`. **DB:** SQLite via `modernc.org/sqlite` (pure Go).
- **Auth:** bcrypt + Bearer tokens. **Deploy:** Docker on k3s (felhom-system ns).
- **Storage:** Longhorn PVC at `/data/` (SQLite DB). **Config:** YAML via ConfigMap at `/etc/felhom-hub/hub.yaml`.
- **Language:** Go (stdlib `net/http` + `html/template`, no frameworks). **DB:** SQLite via
`modernc.org/sqlite` (pure Go). **Auth:** bcrypt + Bearer tokens + session cookies + CSRF.
- **Deploy:** Docker on k3s (`felhom-system` ns). **Storage:** Longhorn PVC at `/data/` (SQLite DB).
- **Config:** YAML via ConfigMap at `/etc/felhom-hub/hub.yaml`. Secrets via out-of-band
`secretKeyRef` (never inline stringData — REUSE.md §3).
## SSH access
Use the Windows OpenSSH binary (Git Bash's `/usr/bin/ssh` can't reach the Windows agent and fails silently): `SSH=/c/Windows/System32/OpenSSH/ssh.exe`. All SSH commands below use `$SSH`.
Use the Windows OpenSSH binary (Git Bash's ssh fails silently): `SSH=/c/Windows/System32/OpenSSH/ssh.exe`.
| Host | IP | User | Role |
|------|----|------|------|
| Build server (k3s node) | 192.168.0.180 | kisfenyo | Build + push images, kubectl (needs `sudo`) |
| Demo Proxmox host | 192.168.0.162 | root@pam (SSH alias felhom-pve, root, no sudo) | pveum/pct + live Proxmox validation — available to CC |
| Build server (k3s node) | 192.168.0.180 | kisfenyo | Build + push images, `sudo kubectl` |
| Demo Proxmox host | 192.168.0.162 | root@pam (SSH alias `felhom-pve`) | pveum/pct + live Proxmox validation |
## Build & deploy — Hub (GitOps via ArgoCD)
The whole k3s cluster is GitOps via a **single ArgoCD app named `felhom`** (`argocd.dooplex.hu`) that syncs this repo's **`manifests/`** to the **`felhom-system`** namespace. **There is no separate `hub` ArgoCD app** — the hub is one `Deployment` (`manifests/hub.yaml`) *inside* the `felhom` app. **Auto-sync is OFF**: deploys are a deliberate manual sync. ArgoCD's source of truth is the **manifest**, so:
**Full runbook: use the `felhom-build-deploy` skill.** The load-bearing rules:
- **A code change + CHANGELOG version bump does NOT deploy anything.** The running image only changes when `manifests/hub.yaml`'s `image:` tag changes in git and the app is synced.
- **Pin explicit versions, never `:latest`.** A `:latest` re-push wouldn't change the manifest, so ArgoCD wouldn't redeploy, and Synced / History / Rollback would all misreport what's actually live.
The whole cluster is GitOps via a **single ArgoCD app `felhom`** syncing this repo's `manifests/`
to `felhom-system`. **Auto-sync is OFF**deploys are a deliberate manual sync. ArgoCD's source of
truth is the **manifest**:
After a code change to `hub/`, to deploy:
- **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 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/`.
1. **Commit + push the code:** `cd /e/git/felhom.eu && git add -A && git commit -m "<msg>" && git push`
2. **Build + push the image** (build script lives on the build server, not in this repo): `$SSH kisfenyo@192.168.0.180 "cd ~/build/felhom-hub && ./build.sh <NEW_VERSION> --push"` (pulls latest from Gitea, builds version into `main.Version` via ldflags, pushes `gitea.dooplex.hu/admin/felhom-hub:<VER>`). Pin `<VER>`; don't rely on `:latest`.
3. **Bump the manifest:** set the `image:` tag in `manifests/hub.yaml` to `:<NEW_VERSION>`, commit to `main`, push. The `felhom` app now shows **OutOfSync**.
4. **Sync** (auto-sync is off, so this is required). Easiest is the ArgoCD UI → app `felhom`**Sync**. From the shell, the `argocd` CLI on 180 is **not logged in** (no server session) and `--core` looks in the wrong namespace under `sudo` (env is stripped) — so the reliable scripted path is to drive the Application CR with `kubectl`:
```bash
# a) hard-refresh so ArgoCD picks up the new commit, then confirm OutOfSync:
$SSH kisfenyo@192.168.0.180 "sudo kubectl -n argocd annotate application felhom argocd.argoproj.io/refresh=hard --overwrite; sleep 8; sudo kubectl -n argocd get application felhom -o jsonpath='{.status.sync.status} {.status.sync.revision}{\"\n\"}'"
# b) trigger the sync via the .operation field (the app controller runs it):
$SSH kisfenyo@192.168.0.180 "sudo kubectl -n argocd patch application felhom --type merge -p '{\"operation\":{\"initiatedBy\":{\"username\":\"cc\"},\"sync\":{\"syncStrategy\":{\"apply\":{}}}}}'"
```
(If you do log the CLI in: `argocd app sync felhom` is the one-liner equivalent.)
5. **Verify:** `$SSH kisfenyo@192.168.0.180 "sudo kubectl -n argocd get application felhom -o jsonpath='sync={.status.sync.status} health={.status.health.status}{\"\n\"}'; sudo kubectl -n felhom-system rollout status deploy/hub --timeout=90s; sudo kubectl -n felhom-system get deploy hub -o jsonpath='{.spec.template.spec.containers[0].image}'; echo; sudo kubectl -n felhom-system logs -l app=hub --tail 10"` (expect Synced/Healthy + the new tag + `[INFO] felhom-hub <VERSION> starting`).
> A bare `kubectl set image` would be reverted on the next sync (the manifest is the truth) — always go through `manifests/hub.yaml`. **The live image can lag the CHANGELOG** when version bumps were committed but step 3/4 was never done; reconcile via the manifest, not by assuming the changelog reflects what's running.
Steps: commit+push code → `./build.sh <VER> --push` on 180 (`~/build/felhom-hub`) → bump
`manifests/hub.yaml` tag + push → ArgoCD hard-refresh + sync (kubectl-patch method in the skill) →
verify Synced/Healthy + rollout + image + startup log.
## Build & deploy — Website / Manifests
- **Website** auto-deploys via git-sync; just push to `main` (live in 12 min). Emergency edits: FileBrowser at `https://files.felhom.eu`.
- **Manifests** (`manifests/`) are GitOps via the `felhom` ArgoCD app — commit to `main`, then sync (auto-sync is off): UI Sync or `argocd app sync felhom`. Do **not** `kubectl apply` them directly (a later sync reverts drift; the manifest in git is the truth).
- **Website** auto-deploys via git-sync; just push to `main` (live in 12 min). **Run
`python scripts/site_gates.py` after ANY website change**; new pages go into its `PAGES` list.
Emergency edits: https://files.felhom.eu. All `website/` HTML is **UTF-8 with BOM** — preserve it.
- **Manifests** are GitOps via the `felhom` app — commit to `main`, then deliberate sync.
## Key patterns
- Hub ingests **host-reports from agents** (`POST /api/v1/host-report`, Bearer per-host) and legacy **controller reports** (`POST /api/v1/report`). The host-report `received_at` is the dead-man's-switch liveness signal.
- Status logic: OK (report < 30m), WARN (30m1h or health=warn), DOWN (> 1h or health=fail).
- SQLite timestamps vary in format — use `parseSQLiteTime()`.
- Dashboard/detail auto-refresh every 60s via `<meta http-equiv="refresh">`. Geo-restricted to Hungary via nginx ingress annotation.
## File encoding
All `website/` HTML is **UTF-8 with BOM** — preserve it. Hub Go source is standard UTF-8 (no BOM).
- Hub status logic: OK (report < 30m), WARN (30m1h or health=warn), DOWN (> 1h or health=fail);
host liveness thresholds shared between UI and checker (never invent a second definition).
- SQLite timestamps vary in format — always `parseSQLiteTime()`.
- Dashboard/detail auto-refresh every 60s via meta refresh. Geo-restricted to Hungary via nginx
ingress annotation.
- Helpers, seams, extension points, traps: **`REUSE.md`** — the map is maintained same-commit.
+8
View File
@@ -3,4 +3,12 @@
> 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 — hub v0.31.0 LIVE** (critical-severity ingest fix + UI badges; ArgoCD Synced/Healthy;
live probe: critical event stored + operator email delivered).
- **2026-07-03 — Felhom skills exist**: `skills/{felhom-build-deploy,felhom-ui-design,felhom-testing}`,
installed via `scripts/install_skills.py`**junction mode** (repo edits live immediately; no
re-install needed after editing). Fresh-session discovery verified.
- **2026-07-03 — CLAUDE.md refreshed** (all four repos): no version-pinned state in CLAUDE.md —
current state lives here / CHANGELOG / REUSE.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.
+81 -79
View File
@@ -4,104 +4,106 @@
---
# REPORT — docs: REUSE.md introduced across all four felhom repos (consolidated)
# REPORT — hub v0.31.0 critical-severity fix + Felhom skills + CLAUDE.md refresh (consolidated)
**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.
**Date:** 2026-07-03 · **Class:** implementation (hub v0.31.0, deployed) + docs/skills across all repos
## 1. Baselines used (all matched the task's expected hashes; trees clean, pulled fresh)
## 1. Baselines & commits
| Repo | `main` @ baseline | State |
All four repos matched the task's expected baselines (pulled clean).
| Repo | Baseline | Commits pushed to `main` |
|---|---|---|
| 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 |
| felhom.eu | `d331eb26d1` | `b5f0050` (fix+tests+REUSE), `4a18306` (manifest→0.31.0), `ad61e96` (render test), + the docs/skills commit carrying this REPORT |
| felhom-controller | `eda33400da` | `a518023` (CLAUDE.md refresh) |
| felhom-agent | `2a024b6039` | `3e760a5` (CLAUDE.md refresh) |
| app-catalog-felhom.eu | `c5a3d1b15b` | `15cdf7b` (CLAUDE.md expansion) |
## 2. Files created/modified per repo
## 2. Part 1 — hub v0.31.0 (the critical-severity fix)
- **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`.
**Change:** `handleEvent` severity switch gains `"critical"` (unknown values still coerce to `"info"`,
exact-match lowercase); `severityLabels["critical"] = "Kritikus hiba"` added; dashboard consumer
gains `EventCriticals`; critical badge renders FIRST in dashboard + customer-page count chains;
`style.css` defines the previously-referenced-but-undefined `.severity-critical` (`--crit` tokens)
and `.severity-ok` (neutral). Frozen legacy `/notify` path and `allowedEventTypes` untouched.
## 3. Commits pushed to `main` (one per repo)
**Tests (new `hub/internal/api/event_test.go` — first tests on /event):**
- A: critical preserved to store — **companion red-proof run:** with the one-line fix reverted the
test FAILED with `stored severity = "info", want "critical"`; fix restored → PASS.
- B: severity `"banana"` → stored `"info"`. C: unknown event_type → 400 + store empty. Auth: 401.
- D: `TestTemplates_DashboardCriticalBadge` (render_test.go) asserts the rendered HTML contains the
`severity-critical` count badge ordered before the error badge. **Method note:** UI verification
used the Go render test — Chrome verification stopped at the operator login (password entry is
out of CC's scope).
- Green gate: `go build ./... && go vet ./... && go test ./...` — all green.
| Repo | Commit |
|---|---|
| felhom-controller | `eda33400da0fd6a3b80271c44df24a177f7a444a` |
| felhom-agent | `2a024b603973e79a0c84d6847a7cde0898091c61` |
| app-catalog-felhom.eu | `c5a3d1b15b71103a3d1da217f1ff5a834857375c` |
| felhom.eu | the commit carrying this REPORT (HEAD of `main` after this push) |
**Deploy (GitOps):** image `felhom-hub:0.31.0` built+pushed on 180; `manifests/hub.yaml` bumped;
ArgoCD app `felhom` hard-refresh + sync → `Synced`/`Healthy`, rollout complete, pod image
`...felhom-hub:0.31.0`, clean startup logs.
## 4. Per-repo sweep counts (evidence of depth)
**Live probe (real pipeline):** POSTed one `event_type:"test"`, `severity:"critical"` event from
guest 9201 using its own controller.yaml credentials (key never echoed — length-only). Result:
HTTP 200; hub log `Event from demo-felhom: test (critical)` (stored severity critical, not info);
dispatcher sent the operator email (`Test email sent to nagyfenyvesi.viktor@gmail.com`).
| 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 |
**REUSE.md** §1 gotcha + §3 trap row updated in the same commit as the fix;
`reuse_refs_check.py``OK [felhom.eu]: 31 cited paths, all exist`.
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.
## 3. Part 2 — skills
## 5. reuse_refs_check.py output (final, all four repos)
Three skills authored in `felhom.eu/skills/` (each SKILL.md ≤160 lines, pushy trigger descriptions):
`felhom-build-deploy`, `felhom-ui-design`, `felhom-testing`. Content verified against live
source/infra before writing (see §5). New `scripts/install_skills.py`: idempotent, junction-first
(`mklink /J`) with copy fallback + re-run reminder.
**Install mode: JUNCTION** — created for all three; second run detected them and no-op'd.
**Discovery (Scenario E):** fresh `claude -p` session listed all three:
```
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
felhom-build-deploy
felhom-testing
felhom-ui-design
```
Bonus trigger probe: "which skill to deploy a new hub version?" → answered `felhom-build-deploy`
with the correct first command (`./build.sh <VER> --push` on 180).
(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.)
## 4. Part 3 — CLAUDE.md refresh
## 6. Observations — doc↔code drift & findings (recorded, NOT acted on)
Standing rule adopted everywhere: **CLAUDE.md carries no version-pinned current state** (that lives
in CONTEXT/CHANGELOG/REUSE); deep runbooks live in the skills, CLAUDE.md keeps a summary table + pointer.
**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.
| File | Diffstat | Headlines |
|---|---|---|
| felhom-controller/CLAUDE.md | +119/294 (338→~160 lines) | full 30-package map (was 7); stale `/opt/docker` demo deploy replaced with verified 9201 bootstrap flow; hub build section deleted; patterns pruned to 6 invariants |
| felhom-agent/CLAUDE.md | +86/71 | "Current: v0.31.0" narrative + slice history deleted (30 versions stale); layout completed (+8 packages, opsign); deploy facts re-verified on felhom-pve |
| felhom.eu/CLAUDE.md | +73/58 | "Hub — current state (v0.7.x)" (23 versions stale) → version-free architecture section; GitOps rules kept incl. live-image-lag note; skills section added |
| app-catalog CLAUDE.md | +26/5 | stub → ~30-line proper file (purpose, deploy contract, pointers) |
| E:\git\CLAUDE.md (local) | +8 | Skills section only |
**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).
## 5. Commands/facts re-verified against live infra (stale findings)
**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).
- Guest 9201 deploy mechanism confirmed live (`/etc/felhom-controller-image` = 0.98.3 tag,
`felhom-controller-bootstrap.service` enabled, container healthy) — the OLD controller CLAUDE.md
still documented the pre-re-platform `/opt/docker` compose deploy for node 1: **stale, replaced**.
- Agent service confirmed on felhom-pve: `User=felhom-agent`,
`ExecStart=/usr/local/bin/felhom-agent --config /etc/felhom-agent/agent.json`, live 0.61.0 — the
OLD agent CLAUDE.md claimed v0.31.0 current: **stale, removed**.
- Build dirs on 180 confirmed: `~/build/{felhom-agent,felhom-controller,felhom-hub}` + `~/git`
clones; Go present.
- Design tokens confirmed byte-identical across website site.css / hub style.css / controller
style.css `:root` blocks (values inlined in the ui-design skill).
- Hub GitOps flow re-verified by executing it (this deploy). Red-proof citation
(`dispatcher_test.go` ~L2749) verified in source.
- Website displays no hub version anywhere (checked) — no website change needed.
## 7. Maintenance hooks now active
## 6. Observations (noticed, NOT acted on)
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).
- The hub severity badge classes predating this task (`.severity-error/-warning/-info`) use legacy
raw hexes, not the v2 `--warn/--crit` tokens — left alone per the no-restyle rule; a token
migration is a candidate D-series follow-up.
- The hub login page appears unstyled relative to the v2 design system (default-blue button, white
background) — pre-existing, out of scope.
- `manifests/felhom.secret.yaml` inline credentials (from the REUSE report) remain an open operator
follow-up.
- The controller currently emits no `critical`-severity events; the fix future-proofs the channel
(hub-internal `host_disk_critical`/`storage_fill_critical` already flow at critical severity —
they bypass ingest).
+12
View File
@@ -1,5 +1,17 @@
# Felhom Hub — Changelog
## docs — Felhom skills introduced + CLAUDE.md refresh (2026-07-03)
Repo-level docs work alongside v0.31.0 (no hub code in this entry):
- **`skills/`** (new, repo root): three versioned Claude Code skills — `felhom-build-deploy`
(per-artifact runbooks, all commands verified live), `felhom-ui-design` (v2 tokens + gates),
`felhom-testing` (non-hollow doctrine + red-proof procedure). Installed to `~/.claude/skills/`
via `scripts/install_skills.py` (junction mode verified).
- **CLAUDE.md refresh:** the "Hub — current state (v0.7.x)" narrative (stale by ~23 versions)
replaced with a version-free architecture section; standing rule adopted — CLAUDE.md carries NO
version-pinned state (that lives in CONTEXT/CHANGELOG/REUSE); skills pointers added. Same rule
applied to the sibling repos' CLAUDE.md in their own commits.
## v0.31.0 — critical severity accepted at event ingest + visible in UI (2026-07-03)
Fixes the gotcha the REUSE sweep surfaced: `handleEvent` coerced any severity outside
+8
View File
@@ -1,5 +1,13 @@
# Felhom scripts — Changelog
## install_skills.py — new: Claude Code skills installer (2026-07-03)
Installs `skills/*/SKILL.md` (felhom-build-deploy, felhom-ui-design, felhom-testing) into
`~/.claude/skills/` as Windows junctions (`mklink /J`) so repo edits are live immediately; falls
back to a full copy if junction creation fails or isn't followed (copy mode prints a re-run
reminder). Idempotent — re-runs detect a correct junction and leave it. Verified: junctions ARE
followed by Claude Code skill discovery (fresh-session probe found all three).
## 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
+77
View File
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""Install the Felhom Claude Code skills from <repo>/skills/ into ~/.claude/skills/.
Usage: python scripts/install_skills.py (run from the felhom.eu repo root or anywhere)
Personal skills (~/.claude/skills/) apply across all projects the right scope for the 4-repo
E:\\git workspace. Preferred install is a Windows junction (mklink /J) so edits in the repo are live
immediately; if junction creation fails or isn't followed, falls back to a full COPY — in copy mode
you must RE-RUN this script after editing skills/. Idempotent: safe to re-run any time.
"""
import os, shutil, subprocess, sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC = os.path.join(REPO, "skills")
DST = os.path.join(os.path.expanduser("~"), ".claude", "skills")
fails = 0
copy_mode_used = False
def is_working_junction(target, src):
"""True if target resolves into src (junction/symlink already correct)."""
try:
return os.path.isfile(os.path.join(target, "SKILL.md")) and \
os.path.samefile(os.path.join(target, "SKILL.md"), os.path.join(src, "SKILL.md"))
except OSError:
return False
def remove_existing(target):
# a junction must be removed with rmdir semantics (never recurse INTO it), a copy with rmtree
try:
os.rmdir(target) # works for junctions and empty dirs
except OSError:
shutil.rmtree(target, ignore_errors=True)
def install(name):
global fails, copy_mode_used
src = os.path.join(SRC, name)
target = os.path.join(DST, name)
if os.path.exists(target):
if is_working_junction(target, src):
print("OK %-22s junction (already installed, live-linked to repo)" % name)
return
remove_existing(target)
# try junction first
r = subprocess.run(["cmd", "/c", "mklink", "/J", target, src],
capture_output=True, text=True)
if r.returncode == 0 and is_working_junction(target, src):
print("OK %-22s junction -> %s" % (name, src))
return
# fall back to copy
if os.path.exists(target):
remove_existing(target)
try:
shutil.copytree(src, target)
copy_mode_used = True
print("OK %-22s COPY (junction failed: %s)" % (name, (r.stderr or r.stdout).strip() or "not followed"))
except OSError as e:
fails += 1
print("FAIL %-22s %s" % (name, e))
if not os.path.isdir(SRC):
print("FAIL: no skills/ dir at %s" % SRC)
sys.exit(2)
os.makedirs(DST, exist_ok=True)
names = sorted(d for d in os.listdir(SRC) if os.path.isfile(os.path.join(SRC, d, "SKILL.md")))
if not names:
print("FAIL: no skills found under %s" % SRC)
sys.exit(2)
for n in names:
install(n)
if copy_mode_used:
print("\nNOTE: copy mode active for at least one skill — RE-RUN this script after editing skills/.")
sys.exit(1 if fails else 0)
+103
View File
@@ -0,0 +1,103 @@
---
name: felhom-build-deploy
description: Build, deploy, publish, or verify ANY Felhom artifact — felhom-controller image (guest 9201 bootstrap deploy), felhom-agent binary (felhom-pve), felhom-hub (GitOps/ArgoCD), the felhom.eu website (git-sync), or the app catalog. Use whenever the task says build, deploy, ship, release, publish, bump version, restart the controller/agent/hub, or verify what version is live. Contains the exact verified commands and the gotchas that silently break deploys.
---
# Felhom build & deploy runbooks
All repos live in `E:\git\` (Git Bash: `/e/git/`). Trunk-based: commit+push to `main` first, always.
Update the repo's `CHANGELOG.md` (+ `REUSE.md` if a shared helper changed) in the same commit.
## Session invariants (set once, every session)
```bash
SSH=/c/Windows/System32/OpenSSH/ssh.exe # Git Bash's /usr/bin/ssh can't reach the Windows agent — fails silently
export MSYS_NO_PATHCONV=1 # before any ssh felhom-pve command with absolute paths (pct etc.)
```
| Host | Access | Role |
|---|---|---|
| Build server (k3s) | `$SSH kisfenyo@192.168.0.180` | build+push images/binaries (`~/build/felhom-{controller,hub,agent}`), `sudo kubectl` |
| Demo Proxmox host | `$SSH felhom-pve` (root@192.168.0.162) | agent deploy, `pct` into guests |
| Demo guest 9201 | via `pct exec 9201 -- bash -c '...'` on felhom-pve | the live controller |
| felhotest (legacy) | `$SSH -p 33022 kisfenyo@router.abonet.hu` | OLD /opt/docker compose mechanism — not the 9201 flow |
Version bumps are ldflags-only (`-X main.version` / `-X main.Version`) — build scripts inject them; no source edit.
## Controller (felhom-controller → guest 9201)
9201 is golden/bootstrap-managed — **NO compose file**. `felhom-controller-bootstrap.service` docker-runs
the tag written in `/etc/felhom-controller-image` (anonymous Gitea pull). Data volume + encryption key persist.
```bash
# 1. commit+push the repo
# 2. build+push image (build.sh does NOT git-pull — the explicit pull is load-bearing)
$SSH kisfenyo@192.168.0.180 "cd ~/build/felhom-controller && git -C ~/git/felhom-controller pull && ./build.sh <VER> --push"
# 3. deploy in the guest
$SSH felhom-pve "pct exec 9201 -- bash -c 'docker pull gitea.dooplex.hu/admin/felhom-controller:<VER> && echo gitea.dooplex.hu/admin/felhom-controller:<VER> > /etc/felhom-controller-image && systemctl restart felhom-controller-bootstrap.service'"
# 4. verify
$SSH felhom-pve "pct exec 9201 -- docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'"
```
Check current live version first: same `docker ps` command, or `cat /etc/felhom-controller-image`.
## Agent (felhom-agent → felhom-pve)
Runs as the NON-ROOT `felhom-agent` user: `/usr/local/bin/felhom-agent --config /etc/felhom-agent/agent.json`
(systemd `felhom-agent.service`). Sudoers allowlist at `/etc/sudoers.d/felhom-agent`.
```bash
# build on 180 (pull first!)
$SSH kisfenyo@192.168.0.180 "cd ~/git/felhom-agent && git pull && go build -ldflags '-X main.version=<VER>' -o /tmp/felhom-agent-<VER> ./cmd/felhom-agent"
# fetch to local, then push to the PVE host (Windows scp needs cygpath -w for the LOCAL path)
scp kisfenyo@192.168.0.180:/tmp/felhom-agent-<VER> "$(cygpath -w /tmp/felhom-agent-<VER>)"
scp "$(cygpath -w /tmp/felhom-agent-<VER>)" felhom-pve:/tmp/
# install with backup + restart
$SSH felhom-pve "cp /usr/local/bin/felhom-agent /usr/local/bin/felhom-agent.bak-\$(/usr/local/bin/felhom-agent --version | awk '{print \$2}') && install -m0755 /tmp/felhom-agent-<VER> /usr/local/bin/felhom-agent && systemctl restart felhom-agent && sleep 3 && /usr/local/bin/felhom-agent --version && journalctl -u felhom-agent -n 20 --no-pager"
```
**Ship the sudoers + guarded-mkfs wrapper WITH the binary whenever `configs/` changed** — several Go
guards exist only if the deployed sudoers/wrapper match the binary (globs must match `stageTemp`
patterns). Beware CRLF when scp-ing config files through Windows. After restart, check the journal
for a clean `ReassertGuestBinds` and no capability-probe degradations.
Publish to Gitea (so Day-0 self-install can fetch it): `scripts/publish-agent.sh <ver> <binary>` with
`REGISTRY_*` creds. The hub's Day-0 artifact manifest must then vouch the new version — that UI is
operator-password-gated (CC cannot); flag it as an operator follow-up.
## Hub (felhom.eu/hub → k3s, GitOps via ArgoCD app `felhom`)
**The manifest is the truth.** A code push + image build deploys NOTHING until `manifests/hub.yaml`'s
`image:` tag changes in git AND the app is synced (auto-sync is OFF). Never `kubectl set image`
(reverted on next sync), never `:latest`. The live image can lag the CHANGELOG — reconcile via the manifest.
```bash
# 1. commit+push code 2. build+push image
$SSH kisfenyo@192.168.0.180 "cd ~/build/felhom-hub && ./build.sh <VER> --push"
# 3. bump manifests/hub.yaml image tag → <VER>, commit, push
# 4. hard-refresh + sync (argocd CLI on 180 is not logged in — drive the Application CR)
$SSH kisfenyo@192.168.0.180 "sudo kubectl -n argocd annotate application felhom argocd.argoproj.io/refresh=hard --overwrite; sleep 8; sudo kubectl -n argocd get application felhom -o jsonpath='{.status.sync.status} {.status.sync.revision}{\"\n\"}'"
$SSH kisfenyo@192.168.0.180 "sudo kubectl -n argocd patch application felhom --type merge -p '{\"operation\":{\"initiatedBy\":{\"username\":\"cc\"},\"sync\":{\"syncStrategy\":{\"apply\":{}}}}}'"
# 5. verify: Synced/Healthy + rollout + image tag + startup log
$SSH kisfenyo@192.168.0.180 "sudo kubectl -n argocd get application felhom -o jsonpath='sync={.status.sync.status} health={.status.health.status}{\"\n\"}'; sudo kubectl -n felhom-system rollout status deploy/hub --timeout=90s; sudo kubectl -n felhom-system get deploy hub -o jsonpath='{.spec.template.spec.containers[0].image}'; echo; sudo kubectl -n felhom-system logs -l app=hub --tail 10"
```
Green gate before any hub commit: `go build ./... && go vet ./... && go test ./...` in `hub/`.
## Website (felhom.eu/website)
Push to `main` = deployed (git-sync sidecar, live in ~12 min). **Run `python scripts/site_gates.py`
first, after ANY website change** (BOM, emoji, nav parity, cache-bust `?v=N` — bump it when css/svg
change). New pages must be added to the script's `PAGES` list. Emergency edits: https://files.felhom.eu.
## App catalog (app-catalog-felhom.eu)
Push to `main` = deploy: the controller's git-sync picks it up within 15 min, or trigger via the
dashboard "Sablonok frissítése" button / `POST /api/sync` (30s debounce). Only `docker-compose.yml` +
`.felhom.yml` sync; deployed `app.yaml` is never overwritten. Conventions: `<repo>/REUSE.md`.
## Other k8s manifests (felhom.eu/manifests)
Same GitOps rule as the hub: edit in git, push, deliberate ArgoCD sync of app `felhom`. Never
`kubectl apply` directly. Secrets: out-of-band `kubectl create secret` + `secretKeyRef` — never inline
`stringData` (see felhom.eu/REUSE.md §3).
+69
View File
@@ -0,0 +1,69 @@
---
name: felhom-testing
description: Felhom testing doctrine — use when writing or reviewing ANY Go test in felhom-controller, felhom-agent, or the felhom.eu hub, and for EVERY correctness or security fix (the red-proof is mandatory there). Triggers - "write a test", "add tests", reviewing a diff that changes logic, fixing a bug, hardening a guard, or validating a fix live. Contains the non-hollow rules, the companion red-proof procedure, seam locations, and the green-gate command.
---
# Felhom testing doctrine
## Non-hollow rule (the cardinal one)
A test must assert the **effect**, not the absence of error. Wrong: HTTP 200 came back, `err == nil`,
"function ran". Right: the stored row has the expected value, the rendered HTML contains the badge,
the state file transitioned, the fake recorded the exact command. If deleting the fix wouldn't fail
the test, the test is hollow.
## Companion red-proof (mandatory for every correctness/security fix)
Prove the test detects the bug it guards against:
1. Temporarily restore the PRE-FIX shape (revert the fixed line, or model the old predicate inline).
2. Run the test → it must **FAIL**, with the wrong value visible in the failure message.
3. Restore the fix → test passes. `git diff` clean.
4. Record the red-proof outcome in `REPORT.md` (what failed, with what value).
In-tree exemplars (verified):
- `felhom.eu/hub/internal/notify/dispatcher_test.go` `TestSeverityNotifies` (~L2749) — models the
pre-fix `warning||error` predicate inline and asserts the fix routes what it dropped.
- `felhom.eu/hub/internal/api/event_test.go` `TestHandleEvent_CriticalPreserved` — asserts the STORED
severity; its red-proof was run by reverting the one-line switch (stored `"info"` → FAIL).
## Seams over shell-outs
Never let a unit test touch docker/pct/real /dev. Every repo's **`REUSE.md` §4** lists its seams and
existing fakes — inject there:
- controller: `diskAgent` (`mockAgent`), `quiesce.Backend/Stacks`, `channelhealth.Probe/Sink`,
`selfupdate.AgentSwapper`, `offboxRunner`, `bootstrap.PullFunc`.
- agent: `proxmox.Runner` (`mockRunner`), `storage.HostOps/HostReader`, localapi `Options` fakes,
Server seam funcs (`reresolveWipe`, `deviceDurableID` — override in tests, no real /dev).
- hub: `Dispatcher.sendEmailFn`, `mailrelay.Sender`, `mailRateLimiter.now` (clock),
`monitor.EventNotifyFunc`, provider interfaces on the api Handler.
Test harness conventions: real store on `t.TempDir()` DB (`hub/internal/api/host_test.go`
`newTestHandler` pattern); `t.Cleanup` for teardown; table-driven where natural; pure
classifier functions get fixture tables (agent `classifyClaim` style).
## What every test suite should also cover
- **Negative cases:** the 400/401/403/refusal paths, not just the happy path (e.g. unknown
event_type → 400 AND nothing stored).
- **Idempotency:** re-running the op is a clean no-op where the contract says so (registry add,
intent set, mount ensure).
- **Fail-safe direction:** for guards, ambiguity must refuse (agent claim/role classifiers are the
canon — any read error ⇒ most-protected verdict).
## Green gate (run before every commit that touches Go code)
```bash
go build ./... && go vet ./... && go test ./...
```
Run it in the module dir: `felhom.eu/hub/`, `felhom-controller/controller/`, `felhom-agent/` root.
Known flake: agent `TestGenerateRecoveryCode_EntropyAndFormat` fails ~1/5 (hyphenated wordlist word) —
re-run before diagnosing; it is not a regression.
## Live validation doctrine (after unit-land)
Exercise the SERVER-SIDE PIPELINE a real user triggers, end-to-end — never hand-set state around it
(the F9 lesson). Invoking the exact endpoint the UI invokes is an acceptable proxy when a browser
isn't available; the residual is client-side rendering only — SAY which method was used. Test crash
behavior with kill -9 / OOM, never `docker kill` (containers' restart policy masks the difference).
Don't echo API keys/tokens into logs or REPORT — extract into shell vars, print lengths only.
+71
View File
@@ -0,0 +1,71 @@
---
name: felhom-ui-design
description: The Felhom design system v2 — use for ANY work on controller dashboard templates/CSS, hub web UI, the felhom.eu website HTML/CSS, customer-facing copy, or status badges/colors. Triggers - editing any *.html/*.css in felhom-controller, felhom.eu hub templates or website; adding a page, badge, button, icon, or color; writing Hungarian customer copy or operator alert text. Contains the token palette, the hard rules (2px/no-shadow/no-emoji/BOM), and which mechanical gate script must run after each surface.
---
# Felhom design system v2
One visual language across three surfaces: `felhom.eu/website/` (public), the hub UI
(`felhom.eu/hub/internal/web/templates/`), the controller UI
(`felhom-controller/controller/internal/web/templates/`). Deep rationale:
`felhom.eu/documentation/_design-review.md`. Canonical patterns per repo: each `REUSE.md` §2.
## Token palette (verified identical in all three `:root` blocks — reconfirm in the target CSS before use)
```css
--bg-0: #0A1220; --bg-1: #0F1B2E; --bg-2: #16263F; /* navy backgrounds, darkest first */
--line: #22344F; --line-soft: #1A2A42; /* hairline borders */
--text-1: #EDF2F9; --text-2: #94A6BF; --text-3: #5E7392; /* text hierarchy */
--blue: #0083D8; --blue-bright: #2EA8F5; --blue-dim: rgba(0,131,216,.13);
--warn: #E0A93E; --warn-dim: rgba(224,169,62,.12);
--crit: #E5534B; --crit-dim: rgba(229,83,75,.12);
--radius: 2px;
--font-ui: 'Plus Jakarta Sans', ...; --font-data: 'JetBrains Mono', ...;
```
Files: `website/assets/site.css`, `hub/internal/web/templates/style.css`,
`controller/internal/web/templates/style.css`. Always tokens — never raw hexes, never inline styles.
## Hard rules
- **Exception-color principle:** healthy/nominal = blue/neutral. Amber (`--warn`) and red (`--crit`)
appear ONLY on deviation. A stopped-but-intentional state is NEUTRAL, not red.
- **Shape:** `--radius` (2px) everywhere; **no box-shadows**; hairline `--line`/`--line-soft` borders;
website sections are boxless (rules + spacing, not cards).
- **Two-tone H1:** last word wrapped in `<span>` (renders `--blue-bright`). H1 only — never H2+.
- **Icons:** monochrome sprite (`icons.svg`, `<use href="...#name">`) / Lucide-style. **No emoji
anywhere** — enforced by gates; never hunt emoji with grep (Windows grep false-negatives multibyte;
Python gates only).
- **Fonts:** vendored woff2 (latin-ext for Hungarian) — **no CDN fonts** (gate-enforced on the website).
- **Language:** Hungarian for everything customer-facing (controller UI, customer emails); English for
operator surfaces (hub UI, operator alerts). Hungarian text lives in maps like `severityLabels` /
`customerMessages` (`hub/internal/notify/templates.go`) — add entries when adding event types.
- **Encoding:** `website/*.html` is UTF-8 **with BOM** (preserve it); Go source + hub/controller
templates are plain UTF-8, no BOM.
- **Cache-bust:** website `site.css` / `icons.svg` references carry `?v=N` — bump N when the asset changes.
## Status vocabularies (class SUFFIXES, defined in the surface's style.css)
- Hub `statusColor`: `nominal / warn / crit / neutral` (server.go funcMap). Severity badges:
`severity-{critical,error,warning,info,ok}`.
- Controller `stateColor`: `run / progress / warn / neutral / off` (funcmap.go). `stateLabel` copy is
frozen byte-identical (unit-tested) — don't reword casually.
- New template funcs go ONLY into the surface's funcMap (hub `server.go` / controller `funcmap.go`).
## Gates — run after every change to the matching surface
| Surface | Gate | When |
|---|---|---|
| website/*.html + site.css | `python scripts/site_gates.py` (in felhom.eu) | after ANY website change; add new pages to its `PAGES` list in the same commit |
| controller templates | `python controller/scripts/template_id_gate.py` + `python controller/scripts/emoji_gate.py` (in felhom-controller) | after ANY controller template change |
| hub templates | render tests: `go test ./internal/web/` (render_test.go, funcmap_test.go) | after template/funcmap changes |
## Canonical patterns to copy (don't reinvent)
- Website page skeleton (nav/footer byte-identical across pages, only `class="active"` differs):
`website/index.html`.
- Hub badge/count chain: `hub/internal/web/templates/dashboard.html` events cell.
- Controller deploy progress-poll panel (3-step, 3s poll): `controller/internal/web/templates/deploy.html`.
- Controller wizard flow (two-step confirm, flash messages via `?flash=`):
`controller/internal/web/storage_handlers.go` + its templates.
- Nested sidebar sub-links: controller `.nav-links-nested` pattern (base template).