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
+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).