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.
13 KiB
CLAUDE.md — Project Instructions for Claude Code (felhom-controller)
Read automatically at session start. Stable orientation only — current state lives in
CONTEXT.mdand the top ofCHANGELOG.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 infelhom.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;mainequals what is deployed. Report-only artifacts →felhom.eu/documentation/(audits/,backlog/). Risky fixes are implemented during the supervised session itself, onmain; 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-designskill; templates must passcontroller/scripts/template_id_gate.py+emoji_gate.py. - Testing doctrine (non-hollow tests, red-proofs, seams): use the
felhom-testingskill. - Logging: new leveled lines use
internal/logx(DEBUG always reaches the debug ring; stdout respectslogging.level); English, keys-never-values, durations on outcomes — full rules infelhom.eu/documentation/runbooks/logging-conventions.md. - Update
REUSE.mdif you added/changed/deprecated a shared helper or pattern (same commit). - Coupled features (controller behavior that depends on a specific agent version): add a
featureProbestable row ininternal/agentapi/features.go+ aSupportsgate call at the feature's entry point; declareMinAgent: X.Y.Zin 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 -cchain and return a false0— which reads exactly like the banner/string being gone. Usekezel,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 neededSSH=/c/Windows/System32/OpenSSH/ssh.exe(Git Bash's ssh lacks the Windows agent and fails silently — seedocs/vscode-ssh-fix.md), plusMSYS_NO_PATHCONV=1forpct 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.162is NOT reachable from DooPlex for the duration. Access via Tailscale: felhom-pve = 100.70.170.35; theHost felhom-pveentry in~/.ssh/configon DooPlex already points there (the direct-LAN path stays available asHost felhom-pve-lan). Delete this block on return. All documentedssh felhom-pve/pct execworkflows are unchanged. Path is direct (not DERP), ~37 ms rtt per hop. At the remote site the host is on DHCP and currently holds192.168.0.147(the guest holds.104); no Pi-hole there — the guest reachesgitea.dooplex.huand*.demo-felhom.euvia public paths. The host agent is DOWN for the duration: itslocalapibinds the literal192.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 --porcelainmust be empty andgit rev-parse HEADmust equalgit rev-parse origin/mainin the repo being built. An unpushed change does not exist — never build a dirty or unpushed tree. Thegit pullin 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 restartdoes NOT pick up new images/env — alwaysup -d(RedeployFromEnv).- Docker's
.Statesays "running" even for unhealthy containers —.Statusparse is the truth. - In-memory
Deployedflag is set BEFOREcompose up -d(slow-pull race); reverted on failure. compose up -dexits 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.mdfor 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
- Commit and push all code changes
- Build, push, and deploy the new controller image (if controller code changed)
- Update CHANGELOG.md with what was done
- Update CONTEXT.md with decisions made, state and what's next
- Update controller/README.md if architecture or features changed
- Verify the deployment is working (check
docker psand logs) - Update REUSE.md if you added/changed/deprecated a shared helper or pattern (same commit)