Files
felhom-controller/CLAUDE.md
T
admin 9f436c8a3b v0.150.0 — green gate restored + the export link stops leaking the CSRF token
F7/R-53: app_export.html built the app's public URL as '<sub>.{{$.CSRFToken}}',
so the "Megnyitás" link was wrong for every app with a subdomain and a session
CSRF token was written into a URL. Template now uses {{$.Domain}}, and
exportPageHandler supplies the key — it builds its own data map instead of
going through baseData, which is where every other page gets it. The page's
real CSRF path (csrfH() reading the meta tag) is correct and untouched.

The 7 red internal/backup tests are green again, with no behaviour change.
TestTier2V2_* / TestSharesTier2* all failed for one environmental reason:
Tier-2's off-drive guard asks system.SamePhysicalDevice (st_dev equality)
whether a target is really a second disk, and every t.TempDir() here shares one
filesystem — so the guard correctly refused the fixture's "two drives" and the
tests never reached their subject ("nincs másik fizikai meghajtó").

Seam in the package's existing style: a nil-defaulted Manager.samePhysicalDevice
field + sameDevice wrapper, seven call sites routed through it. Nil resolves to
system.SamePhysicalDevice, so production is byte-for-byte unchanged; only the two
fixtures inject a fake modelling one drive per directory subtree. No assertion
weakened, nothing skipped/renamed/deleted; all 7 mutation-proved.

Also: the ssh->pct-exec ASCII-grep and heredoc-credential traps are now in
CLAUDE.md's live-validation section.
2026-07-20 09:42:24 +02:00

204 lines
13 KiB
Markdown

# CLAUDE.md — Project Instructions for Claude Code (`felhom-controller`)
> Read automatically at session start. Stable orientation only — **current state lives in
> `CONTEXT.md` and the top of `CHANGELOG.md`**, never here. Cross-repo orientation: workspace-root
> `/mnt/5_hdd/felhom.eu/git/CLAUDE.md`.
!!! IMPORTANT !!!
- Always update CHANGELOG.md whenever you modified the code, and pushed to git!!
- IF controller feature changed (new/modify/remove) always update the relevant part of controller/README.md with the architectural change!!
## Project overview
Felhom is a managed home-server business for Hungarian customers. This repo contains the
**felhom-controller** — the Go application that manages Docker Compose stacks inside each customer
LXC guest via a Hungarian-language web dashboard.
Read in this order:
- **`REUSE.md`** — before writing new code (canonical helpers, patterns, traps, seams).
- `CONTEXT.md` — current project state, decisions, roadmap (update after each session).
- `controller/README.md` — full feature/architecture reference (update when features change).
- `TASK.md` — the current task to implement (if it exists).
## System context — the three-component model
The project runs **on Proxmox**, with a locked three-component model:
- **Hub** (`felhom.eu/hub/`) — operator backend on k3s.
- **Host agent** (`felhom-agent/`) — one per Proxmox host; operator-tier; owns ALL Proxmox interaction.
- **In-guest controller** (THIS repo) — one per customer LXC; **Docker-only; holds NO Proxmox
credentials**. De-privileged: disk/host/Proxmox concerns are delegated to the host agent via the
pinned local-API client (`internal/agentapi`); the controller keeps the app domain — stack/deploy
management, the Hungarian web UI, app-data backup, metrics/telemetry, integrations, git-sync,
notifications. Whole-guest backup (PBS vzdump) is the agent's.
> **Authoritative maps:** `felhom.eu/documentation/architecture/01/02/03-*.md` (topology/trust,
> controller module map, host agent) + the code-verified feature docs in
> `felhom.eu/documentation/controller/`. Match the current code, not summaries, if they drift.
**Don't confuse the two ex-"controllers":** `felhom-agent` (host, operator-tier, was
`proxmox-controller`) vs this `felhom-controller` (in-guest, was `deploy-felhom-compose`).
## Layout (verified against the tree)
```
controller/cmd/controller/ entry point + startup wiring (scheduler block, init-only setters)
controller/internal/
agentapi/ pinned-TLS client to the host agent's per-guest local API (THE disk seam)
api/ REST /api/* router (writeJSON envelope, limitBody, config writes)
appbackup/ felhom-data paths/namespaces, DB dumps, userdata skeleton (shared primitives)
appexport/ .fab export/import bundles (password crypto, strict segment validation)
assets/ app logo/screenshot sync from the hub
backup/ app-data backup manager, recovery units, tier-2 copies, offbox restic
bootstrap/ bootstrap.json ingest → controller.yaml (Day-0 + refresh)
channelhealth/ agent-channel health checker (debounce + born-down alerting)
cloudflare/ geo-enforcement remnant (agent-delegated)
config/ controller.yaml load/validate (LoadPermissive = setup-mode only)
crypto/ AES-256-GCM app.yaml secret encryption (ENC: prefix)
infra/ traefik/cloudflared/filebrowser base-stack templates
integrations/ app-to-app integrations (e.g. OnlyOffice)
mailrelay/ app-email SMTP shim → hub relay
metrics/ telemetry collection
monitor/ health checks, protected containers
notify/ hub event push (typed Notify* wrappers)
quiesce/ quiesce loop for whole-guest backup (marker + recover)
recovery/ recovery-unit restore
report/ hub report builder/pusher + pull-based config refresh
scheduler/ background jobs (Every/Daily, Budapest DST-safe)
selftest/ startup self-checks
selfupdate/ controller image self-update via the agent swap
settings/ settings.json persistence (registry, flags, corruption recovery)
setup/ first-boot setup wizard (own CSRF)
stacks/ compose ops: deploy/delete/migrate/state (THE app domain core)
sync/ git-sync of the app catalog
system/ mounts/probes (linux + permissive _other stubs)
util/ small shared helpers
web/ dashboard UI: server, auth/CSRF, handlers, funcmap, templates (Hungarian)
```
Per-package helpers/seams/traps: **`REUSE.md`** (maintained same-commit as helper changes).
## Conventions & cardinal rules
- **Trunk-based — no branches.** All shippable work commits directly to `main`; `main` equals what is
deployed. Report-only artifacts → `felhom.eu/documentation/` (`audits/`, `backlog/`). Risky fixes
are implemented during the supervised session itself, on `main`; if a fix can't be verified/shipped,
revert + report — never park on a branch.
- Code quality: double-check for bugs/edge cases; add debug logging; **ask rather than guess**.
- All UI text is Hungarian (Budapest timezone). Design tokens/gates: use the `felhom-ui-design`
skill; templates must pass `controller/scripts/template_id_gate.py` + `emoji_gate.py`.
- Testing doctrine (non-hollow tests, red-proofs, seams): use the `felhom-testing` skill.
- **Logging**: new leveled lines use `internal/logx` (DEBUG always reaches the debug ring; stdout
respects `logging.level`); English, keys-never-values, durations on outcomes — full rules in
`felhom.eu/documentation/runbooks/logging-conventions.md`.
- Update `REUSE.md` if you added/changed/deprecated a shared helper or pattern (same commit).
- **Coupled features** (controller behavior that depends on a specific agent version): add a
`featureProbes` table row in `internal/agentapi/features.go` + a `Supports` gate call at the
feature's entry point; declare `MinAgent: X.Y.Z` in the CHANGELOG entry header. Rules:
`felhom.eu/documentation/runbooks/publish-train-rules.md`.
> **In every repository where you make a change, update both files in that repo:**
> - **`CHANGELOG.md`** — cumulative log, newest on top.
> - **`REPORT.md`** — **overwrite** with the most recent implementation/validation summary only.
>
> **Never write secrets** into any committed file — reference them as "stored out-of-band".
## Live validation
Exercise the SERVER-SIDE PIPELINE a real user triggers, end-to-end (connect → enroll → deploy). The
forbidden shortcut is BYPASSING that pipeline (the F9 episode: raw agent guest-attach + hand-set
state). **`claude-in-chrome` is NOT available in the DooPlex environment** — the standard method is
endpoint-level: invoke the exact endpoint the UI invokes (no server logic is skipped, only
rendering) and say which method was used. Strict end-to-end UI coverage is a manual click-through.
Two traps in that method, both from the 2026-07-20 remediation:
- **Grep the fetched page with ASCII-only substrings.** Accented Hungarian patterns get mangled
through the `ssh → pct exec → bash -c` chain and return a false `0` — which reads exactly like the
banner/string being gone. Use `kezel`, `Utols`, `Biztons`; never let an accented pattern gate a
conclusion (it nearly produced a wrong "banner cleared" claim).
- **Credentials with `!` or `'` break in heredoc-built helper scripts** (history expansion eats
`!!`). Use the proven inline `-d "password=$PW"` form for authed curl, and delete any
credential-bearing helper from `/tmp` (host AND guest) when done.
## Environment & access
Claude Code runs **on DooPlex (192.168.0.180, Debian 13, user `kisfenyo`)**; repos in
`/mnt/5_hdd/felhom.eu/git/`, build dirs in `/mnt/5_hdd/felhom.eu/build/`. All repos hosted at
`gitea.dooplex.hu/admin/`. Builds are local commands; felhom-pve is one SSH hop.
| Host | Access | Role |
|------|--------|------|
| **DooPlex (this host)** | local — `/mnt/5_hdd/felhom.eu/{git,build}/` | build + push images, `sudo kubectl` |
| Demo Proxmox host `demo-felhom` | `ssh felhom-pve` (root@192.168.0.162) | `pct` into guests; live validation |
| Demo guest 9201 | `ssh felhom-pve "pct exec 9201 -- ..."` | the live demo controller (golden/bootstrap-managed) |
| felhotest (legacy) | `ssh -p 33022 kisfenyo@router.abonet.hu` | OLD /opt/docker compose mechanism |
> **Legacy: Windows workstation.** Until 2026-07-19 CC ran on Windows 11 with repos in `E:\git\`,
> and every remote command needed `SSH=/c/Windows/System32/OpenSSH/ssh.exe` (Git Bash's ssh lacks
> the Windows agent and fails silently — see `docs/vscode-ssh-fix.md`), plus `MSYS_NO_PATHCONV=1`
> for `pct exec`. Retained in case that environment is revived.
> **TEMPORARY — felhom-pve is at a remote site (until ~2026-08-02).** The home-LAN literal
> `192.168.0.162` is NOT reachable from DooPlex for the duration. Access via Tailscale:
> felhom-pve = 100.70.170.35; the `Host felhom-pve` entry in `~/.ssh/config` on DooPlex already
> points there (the direct-LAN path stays available as `Host felhom-pve-lan`). Delete this block on
> return. All documented `ssh felhom-pve` / `pct exec` workflows are unchanged. Path is **direct**
> (not DERP), ~37 ms rtt per hop. At the remote site the host is on **DHCP** and currently holds
> `192.168.0.147` (the guest holds `.104`); no Pi-hole there — the guest reaches `gitea.dooplex.hu`
> and `*.demo-felhom.eu` via public paths. **The host agent is DOWN for the duration**: its
> `localapi` binds the literal `192.168.0.162`, which no longer exists → `bind: cannot assign
> requested address`, so every agent-backed feature (storage, PBS backup, quiesce, restore-test, DR)
> is unavailable until fixed. Details + findings:
> `felhom.eu/documentation/audits/AUDIT-vacation-remote-ops-2026-07-20.md`
External access via Cloudflare Tunnel → Traefik; Pi-hole forwards `*.demo-felhom.eu` → .162 locally.
## Build & deploy — MANDATORY after code changes
**Full runbook: use the `felhom-build-deploy` skill.** Summary (guest 9201 is bootstrap-managed —
**no compose file**; `felhom-controller-bootstrap.service` runs the tag in `/etc/felhom-controller-image`):
> **Clean-tree gate before any build:** `git status --porcelain` must be empty and
> `git rev-parse HEAD` must equal `git rev-parse origin/main` in the repo being built. An unpushed
> change does not exist — never build a dirty or unpushed tree. The `git pull` in the build step
> stays (it is a no-op when you work in this tree, and load-bearing if anything was pushed from
> elsewhere).
| Step | Command |
|------|---------|
| 1. Commit + push | `git add <explicit paths> && git commit -m "..." && git push` |
| 2. Build + push image | `cd /mnt/5_hdd/felhom.eu/build/felhom-controller && git -C /mnt/5_hdd/felhom.eu/git/felhom-controller pull && ./build.sh <VER> --push` (build.sh does NOT pull — the explicit pull is load-bearing) |
| 3. Deploy (9201) | `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}}'"` + container logs |
Hub build/deploy lives in `felhom.eu` (GitOps) — see that repo's CLAUDE.md / the skill. Catalog
changes (`app-catalog-felhom.eu`): commit+push; controller sync picks them up ≤15 min or via the
"Sablonok frissítése" button.
## Session-critical invariants (the rest live in REUSE.md)
- `docker compose restart` does NOT pick up new images/env — always `up -d` (`RedeployFromEnv`).
- Docker's `.State` says "running" even for unhealthy containers — `.Status` parse is the truth.
- In-memory `Deployed` flag is set BEFORE `compose up -d` (slow-pull race); reverted on failure.
- `compose up -d` exits 0 on crash-loops — post-start status check is the detection.
- Env var KEYS are logged, never values. Protected stacks (traefik, cloudflared, felhom-controller)
can't be stopped from the UI.
- Verify a container image HAS the healthcheck tool before using it (BusyBox wget / python3 / curl —
catalog REUSE.md maps the families).
## Working with CHANGELOG.md
**DO NOT read the full file** — it is large and will waste context.
- Session start: use `CONTEXT.md` + `controller/README.md` for current state.
- Adding an entry: Read only the top ~30 lines for format, then Edit-insert after line 1.
- History: Grep for topics instead of reading.
## End-of-session checklist
1. **Commit and push** all code changes
2. **Build, push, and deploy** the new controller image (if controller code changed)
3. **Update CHANGELOG.md** with what was done
4. **Update CONTEXT.md** with decisions made, state and what's next
5. **Update controller/README.md** if architecture or features changed
6. **Verify** the deployment is working (check `docker ps` and logs)
7. **Update REUSE.md** if you added/changed/deprecated a shared helper or pattern (same commit)