diff --git a/CLAUDE.md b/CLAUDE.md index e4b74a5..471d364 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 "" && 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 --push"` (pulls latest from Gitea, builds version into `main.Version` via ldflags, pushes `gitea.dooplex.hu/admin/felhom-hub:`). Pin ``; don't rely on `:latest`. -3. **Bump the manifest:** set the `image:` tag in `manifests/hub.yaml` to `:`, 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 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 --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 1–2 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 1–2 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 (30m–1h or health=warn), DOWN (> 1h or health=fail). -- SQLite timestamps vary in format — use `parseSQLiteTime()`. -- Dashboard/detail auto-refresh every 60s via ``. 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 (30m–1h 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. diff --git a/CONTEXT.md b/CONTEXT.md index 1316128..25f2657 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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. diff --git a/REPORT.md b/REPORT.md index 9708a4c..0731e2a 100644 --- a/REPORT.md +++ b/REPORT.md @@ -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 --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` ~L27–49) 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 [...]` (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). diff --git a/hub/CHANGELOG.md b/hub/CHANGELOG.md index fd5cab2..a6ef495 100644 --- a/hub/CHANGELOG.md +++ b/hub/CHANGELOG.md @@ -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 diff --git a/scripts/CHANGELOG.md b/scripts/CHANGELOG.md index 355a16d..de8b5a6 100644 --- a/scripts/CHANGELOG.md +++ b/scripts/CHANGELOG.md @@ -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 diff --git a/scripts/install_skills.py b/scripts/install_skills.py new file mode 100644 index 0000000..68c4ffb --- /dev/null +++ b/scripts/install_skills.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +"""Install the Felhom Claude Code skills from /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) diff --git a/skills/felhom-build-deploy/SKILL.md b/skills/felhom-build-deploy/SKILL.md new file mode 100644 index 0000000..4a473f6 --- /dev/null +++ b/skills/felhom-build-deploy/SKILL.md @@ -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 --push" +# 3. deploy in the guest +$SSH felhom-pve "pct exec 9201 -- bash -c 'docker pull gitea.dooplex.hu/admin/felhom-controller: && echo gitea.dooplex.hu/admin/felhom-controller: > /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=' -o /tmp/felhom-agent- ./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- "$(cygpath -w /tmp/felhom-agent-)" +scp "$(cygpath -w /tmp/felhom-agent-)" 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- /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 ` 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 --push" +# 3. bump manifests/hub.yaml image tag → , 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 ~1–2 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: `/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). diff --git a/skills/felhom-testing/SKILL.md b/skills/felhom-testing/SKILL.md new file mode 100644 index 0000000..9069ffd --- /dev/null +++ b/skills/felhom-testing/SKILL.md @@ -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` (~L27–49) — 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. diff --git a/skills/felhom-ui-design/SKILL.md b/skills/felhom-ui-design/SKILL.md new file mode 100644 index 0000000..494dcca --- /dev/null +++ b/skills/felhom-ui-design/SKILL.md @@ -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 `` (renders `--blue-bright`). H1 only — never H2+. +- **Icons:** monochrome sprite (`icons.svg`, ``) / 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).