# CLAUDE.md — `felhom-controller`

> Stable orientation only — **current state lives in `CONTEXT.md` and the top of `CHANGELOG.md`**,
> never here. Cross-repo conventions (clean-tree gate, secrets, trunk-based, artifact taxonomy):
> workspace-root `/mnt/5_hdd/felhom.eu/git/CLAUDE.md`. Path-scoped detail: `.claude/rules/`.

## What this repo is

The **in-guest controller** — one per customer LXC, Docker-only, **holds NO Proxmox credentials**. It
owns the app domain: stack/deploy management, the Hungarian web UI, app-data backup, metrics,
integrations, git-sync, notifications. Disk/host/Proxmox concerns are delegated to the host agent via
`internal/agentapi`. Whole-guest backup (PBS vzdump) is the agent's, not ours.

**Don't confuse the two ex-"controllers":** `felhom-agent` (host, operator-tier, was
`proxmox-controller`) vs this repo (in-guest, was `deploy-felhom-compose`).

## Doing X → read Y

| Doing | Read |
|---|---|
| writing any new code | `REUSE.md` — canonical helpers, patterns, traps, seams |
| needing current state / roadmap | `CONTEXT.md` |
| needing a feature or architecture reference | `controller/README.md` |
| build, deploy, publish, verify a version | the **`felhom-build-deploy`** skill |
| writing or reviewing a test, fixing a bug | the **`felhom-testing`** skill |
| UI, tokens, badges, Hungarian copy | the **`felhom-ui-design`** skill |
| which box may I break | `felhom.eu/documentation/runbooks/target-selection.md` |
| host addresses, break-glass, node facts | `felhom.eu/documentation/operations/nodes.md` |
| what version is live anywhere | ask the hub (`/hosts`, `/configs`) or the box — **never a doc** |
| the authoritative design | `felhom.eu/documentation/architecture/01/02/03-*.md` |

## Session-critical invariants

The rest live in `REUSE.md`. These cost incidents to learn:

- `docker compose restart` does NOT pick up new images/env — always `up -d` (`RedeployFromEnv`).
- Docker's `.State` says "running" even for unhealthy containers — the `.Status` parse is the truth.
- In-memory `Deployed` is set BEFORE `compose up -d` (slow-pull race); reverted on failure.
- `compose up -d` exits 0 on crash-loops — the post-start status check is the detection.
- Env var KEYS are logged, never values. Protected stacks (traefik, cloudflared, felhom-controller)
  cannot be stopped from the UI.
- Verify a container image HAS the healthcheck tool before using it (BusyBox wget / python3 / curl —
  the catalog `REUSE.md` maps the families).
- `IsRunning()` is CONCURRENCY, false during a verification restore — display MUST use
  `RestoreStatus()`.

## Live validation — the fence

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 was a raw agent guest-attach with
hand-set state, and it proved nothing.

`claude-in-chrome` is NOT available on DooPlex. 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 by the operator.

Two traps in that method live in `.claude/rules/ui-hungarian.md` (ASCII-only greps; `!` in
credentials) — they load when you touch a template or stylesheet.

## Commands — one per surface

| Surface | Command |
|---|---|
| Gates (after ANY change) | `python3 controller/scripts/controller_gates.py` — from `controller/` |
| Green gate | `go build ./... && go vet ./... && go test ./...` |
| Build + deploy | the **`felhom-build-deploy`** skill — do not hand-roll it |

Guest 9201 is **bootstrap-managed — there is no compose file**;
`felhom-controller-bootstrap.service` runs the tag written in `/etc/felhom-controller-image`. Catalog
changes (`app-catalog-felhom.eu`) are picked up by controller sync ≤15 min, or via the "Sablonok
frissítése" button.

## Working with CHANGELOG.md

**DO NOT read the full file** — it is large and will waste context.

- Session start: `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 (explicit paths; no `git add -A`).
2. **Build, push, and deploy** the new controller image, if controller code changed.
3. **`CHANGELOG.md`** — always, whenever code changed and was pushed.
4. **`CONTEXT.md`** — decisions made, state, what is next.
5. **`controller/README.md`** — whenever a feature was added, modified or removed.
6. **`REPORT.md`** — overwrite with this run's summary only.
7. **`REUSE.md`** — if a shared helper or pattern was added/changed/deprecated (same commit).
8. **Verify** the deployment (`docker ps` + logs).

<!--
WHY THIS FILE IS SHORT (2026-08-06, instruction-trim task).
Removed from here and rehomed, not lost:
  - the `## Layout (verified against the tree)` block  -> derivable by `ls internal/`; REUSE.md
    carries the per-package seams and traps that the annotations were really for.
  - the `!!! IMPORTANT !!!` header -> its two requirements are checklist items 3 and 5. One voice,
    one place; a rule stated twice in one file is a rule that gets edited in one of them.
  - the host/access table -> documentation/operations/nodes.md is the single home. The copy here
    had drifted: it gave demo-felhom as plain root@192.168.0.162 (the LAN fallback, not the route),
    pinned "agent 0.93.0" against the project's own no-versions-in-docs rule, and claimed no drill
    VM was provisioned on demo-hp. Measured 2026-08-06: `qm list` on demo-hp shows VM 300
    `drill-r50` present. felhom-agent/CLAUDE.md was right; this file was wrong.
  - the "felhom-pve is back on the home LAN" block -> it was bookkeeping about a retired block; the
    record is in documentation/audits/AUDIT-vacation-remote-ops-2026-07-20.md.
  - the "Legacy: Windows workstation" block -> the workspace-root CLAUDE.md carries the full version.
  - the gates/logging/coupling/UI paragraphs -> .claude/rules/*.md, which load when a matching file
    is read instead of in every session.
Full per-block accounting: felhom.eu/documentation/audits/LEDGER-instruction-trim-2026-08-06.md
-->
