# CLAUDE.md — Project Instructions for Claude Code

> This file is read automatically by Claude Code at the start of every session.
> It replaces the "Instructions" panel from the claude.ai Project.
> Keep it updated as the project evolves.

## Project overview

Creating a business (Felhom) for home-server deployment for Hungarian customers. This repository
(`deploy-felhom-compose`) contains the felhom-controller — a Go application that manages Docker
Compose stacks on customer hardware via a Hungarian-language web dashboard.

See `controller/README.md` for full architecture and status.
See `CONTEXT.md` for current project state, recent work, and decisions (update after each session).
See `TASK.md` for the current task to implement (if it exists).

Claude in Chrome extension is available — can be used to test web UI on demo-felhom.eu or verify dashboard deployments in browser.

## Code quality rules

- Always double-check generated code for bugs, logic issues, syntax errors
- Handle edge cases without overcomplicating the script/program
- Add debug capabilities (logging, verbose output) for easier troubleshooting
- If you need more input or troubleshooting command output, ask first — don't guess

## Workspace layout

Claude Code runs on Windows. The working directory is `E:\git\` (mapped as `/e/git/` in Git Bash).
This repo is at:

```
E:\git\deploy-felhom-compose\     (or /e/git/deploy-felhom-compose/ in Git Bash)
├── controller/                    # Go application (main codebase)
│   ├── cmd/controller/            # Entry point (main.go)
│   ├── internal/
│   │   ├── config/                # YAML config loading
│   │   ├── stacks/                # Docker Compose operations, deploy flow
│   │   ├── sync/                  # Git sync — periodic pull of app catalog repo
│   │   ├── api/                   # REST API endpoints
│   │   ├── system/                # System info (memory, disk)
│   │   └── web/                   # Dashboard UI
│   │       ├── server.go          # Server struct, routing, static serving
│   │       ├── auth.go            # Session auth, login/logout handlers
│   │       ├── handlers.go        # Page handlers (dashboard, stacks, deploy, etc.)
│   │       ├── funcmap.go         # Template function map
│   │       ├── embed.go           # go:embed directive for templates
│   │       ├── templates.go       # Felhom logo SVG constant
│   │       └── templates/         # go:embed HTML/CSS files (Hungarian UI)
│   ├── Dockerfile
│   ├── Makefile
│   └── go.mod
├── scripts/                       # Setup scripts for customer nodes
├── CLAUDE.md                      # This file
├── CONTEXT.md                     # Project memory / state
└── TASK.md                        # Current task (if exists)
```

Related repos (same parent directory):
```
E:\git\app-catalog-felhom.eu\     # Docker Compose templates + .felhom.yml metadata per app
E:\git\felhom.eu\                 # Website (htmls) + k3s manifests
E:\git\homelab-manifests\         # k3s cluster manifests (dooplex.hu services)
E:\git\misc-scripts\              # Helper scripts
```

All repos hosted at `gitea.dooplex.hu/admin/`. Git credentials are stored (`git config credential.helper store`).

## SSH access

SSH key-based authentication is configured and working. No password prompts.

| Host | IP | User | Role |
|------|----|------|------|
| Build server | 192.168.0.180 | kisfenyo | Build + push container images |
| Demo node | 192.168.0.162 | kisfenyo | Test deployment (demo-felhom.eu) |

## Test environments

| Node | Hardware | Domain | IP | Notes |
|------|----------|--------|----|-------|
| demo-felhom | Acemagic N100, 16G RAM, 512G SSD + 1TB HDD | demo-felhom.eu | 192.168.0.162 | Primary test node, Cloudflare Tunnel |
| pi-customer-1 | Raspberry Pi 3B+, 1G RAM, 32G SD | pi-customer-1.local | 192.168.0.161 | Secondary test, not yet active |

- Pi-hole DNS on local network forwards `*.demo-felhom.eu` → 192.168.0.162
- External access via Cloudflare Tunnel → Traefik reverse proxy

## Build & deploy workflow — MANDATORY

After making code changes to the controller, you **MUST** build, push, and deploy the new image.
Do NOT leave code changes uncommitted or undeployed. The full cycle is:

### Step 1: Commit and push changes

```bash
cd /e/git/deploy-felhom-compose
git add -A && git commit -m "<descriptive message>" && git push
```

### Step 2: Build + push the container image on the build server

The build server (192.168.0.180) has the build toolchain. The version tag should be incremented
from the current running version.

First, check the current running version:
```bash
ssh kisfenyo@192.168.0.162 "docker ps --filter name=felhom-controller --format '{{.Image}}'"
```

Then build with the next version (e.g., if current is 0.2.10, use 0.2.11):
```bash
ssh kisfenyo@192.168.0.180 "cd ~/build/felhom-controller && git -C ~/git/deploy-felhom-compose pull && ./build.sh <NEW_VERSION> --push"
```

The build script:
- Pulls latest code from Gitea
- Builds a multi-arch Docker image (amd64 + arm64) if `--multiarch`, or current arch if `--push`
- Pushes to `gitea.dooplex.hu/admin/felhom-controller:<VERSION>`
- Expects the version as first argument (e.g., `0.2.11`)

### Step 3: Deploy on the demo node

```bash
ssh kisfenyo@192.168.0.162 "cd /opt/docker/felhom-controller && sudo docker pull gitea.dooplex.hu/admin/felhom-controller:<NEW_VERSION> && sudo sed -i 's|image: gitea.dooplex.hu/admin/felhom-controller:.*|image: gitea.dooplex.hu/admin/felhom-controller:<NEW_VERSION>|' docker-compose.yml && sudo docker compose up -d"
```

### Step 4: Verify the deployment

```bash
ssh kisfenyo@192.168.0.162 "docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'"
```

Should show the new version and "Up" status. Also check logs for startup errors:
```bash
ssh kisfenyo@192.168.0.162 "docker logs felhom-controller --tail 20"
```

### Build workflow summary

| Step | Command | Where |
|------|---------|-------|
| 1. Commit + push | `git add -A && git commit -m "..." && git push` | Local (this repo) |
| 2. Build + push image | `ssh 192.168.0.180 "... ./build.sh <VER> --push"` | Build server |
| 3. Deploy | `ssh 192.168.0.162 "... docker compose up -d"` | Demo node |
| 4. Verify | `ssh 192.168.0.162 "docker ps ..."` | Demo node |

**IMPORTANT:** If you make changes to the app-catalog-felhom.eu repo, commit and push those too:
```bash
cd /e/git/app-catalog-felhom.eu
git add -A && git commit -m "<message>" && git push
```
The controller's git sync will pick up catalog changes within 15 minutes, or you can trigger it
manually via the dashboard "Sablonok frissítése" button.

## Tech stack

- **Language:** Go 1.22+
- **Web framework:** stdlib `net/http` + `html/template` (no frameworks)
- **Templates:** go:embed HTML files in `internal/web/templates/` (Hungarian UI)
- **CSS:** go:embed CSS file in `internal/web/templates/style.css`
- **Auth:** bcrypt password hash + session cookies
- **Container orchestration:** Docker Compose via CLI (`docker compose up -d`)
- **Reverse proxy:** Traefik (separate stack, managed by controller)
- **Tunnel:** Cloudflare Tunnel (cloudflared, separate stack)

## Key patterns

- All UI text is in Hungarian (Budapest timezone, Hungarian locale)
- Templates use Go template functions: `stateColor`, `stateLabel`, `stateIcon`, `stateStr`, `isOperational`, `logoURL`, `logoPNGURL`, `appPageURL`
- Container states: `running`, `starting`, `unhealthy`, `stopped`, `exited`, `restarting`, `paused`, `not_deployed`
- Docker `.State` field is combined with `.Status` field to detect health substatus
- Stacks are sorted alphabetically by DisplayName
- Protected stacks (traefik, cloudflared, felhom-controller) can't be stopped from UI
- `app.yaml` persists deploy config; `deployed: true` flag controls UI state
- In-memory `Deployed` flag is set BEFORE `docker compose up -d` (avoids race condition with slow image pulls); reverted on failure
- Password fields require explicit user input or generation (no silent auto-fill)
- App cards on dashboard and stacks pages are clickable via `data-href` attribute (skip protected stacks)
- Logs page uses AJAX polling (`?raw=1` query param returns plain text) with auto-scroll and pause/resume
- Memory bar on deploy page uses two-segment stacked bar (committed = solid green, new = translucent green)
- Deploy flow shows 3-step progress panel (config → containers → health), polls `GET /api/stacks/{name}` every 3s until running/unhealthy/timeout(120s)
- Telepítés buttons have `checkBeforeDeploy()` onclick guard — fetches live state from API before navigating to deploy page
- App info pages at `/apps/{slug}` — detail view with use cases, setup guide, screenshots, optional config
- Optional config saves to `app.yaml` and restarts deployed apps via `docker compose up -d`
- `optional_config` fields in `.felhom.yml` define post-deploy configurable env vars (e.g., API keys)
- `app_info` in `.felhom.yml` provides tagline, use_cases, first_steps, prerequisites, default_creds, docs_url

## Git sync module (internal/sync)

- Uses `os/exec` to call `git` CLI — no Go git library dependency
- On startup: clones repo to `{data_dir}/catalog-cache/` (shallow clone, `--depth 1`)
- Periodically: `git fetch --depth 1` + `git reset --hard origin/{branch}`
- Copies only `docker-compose.yml` and `.felhom.yml` to stacks dir
- **Never overwrites** `app.yaml` or `.env` — these contain deployed secrets
- Content-hash comparison (SHA-256) — only writes if file actually changed
- After sync, triggers `ScanStacks()` rescan for dashboard update
- `POST /api/sync` triggers immediate sync (30s debounce)
- "Sablonok frissítése" button on Alkalmazások page
- Sync status exposed in `/api/system/info` response

## Debug logging

The controller has two-tier logging controlled by `logging.level` in `controller.yaml` (or `FELHOM_LOGGING_LEVEL` env var):

- **`info`** (default): Operation success/failure with elapsed time, post-start container states, scan counts
- **`debug`**: All of above plus env var keys per compose command, local image availability checks, compose command completion times, log fetch byte counts

Key patterns used in `internal/stacks/`:
- `time.Since(start)` for operation timing — always logged at INFO level
- `m.isDebug()` gates verbose output (env var keys, image checks)
- `truncateStr(s, 500)` caps stdout/stderr in error logs
- `logPostStartStatus()` runs async (goroutine + 3s sleep) after start/restart/update/deploy — never blocks or fails the operation
- `checkLocalImages()` parses compose YAML for `image:` lines, runs `docker image inspect` per image
- Env var **keys** are logged, never values (secrets safety)

## Important lessons learned

1. `PAPERLESS_OCR_LANGUAGES` (plural, with S) **installs** tesseract packs; `PAPERLESS_OCR_LANGUAGE` (singular) **selects** which to use
2. `docker compose restart` does NOT pick up new images — always use `docker compose up -d`
3. Go map iteration order is random — always sort before displaying in UI
4. Docker's `.State` field says "running" even for unhealthy containers — must parse `.Status` for health info
5. In-memory `Deployed` flag must be set BEFORE `docker compose up -d` (not after) — compose can take 30-60s for image pulls; revert both in-memory and disk on failure
6. `docker compose up -d` returns exit 0 even when containers crash-loop — post-start status check is essential for detecting failures
7. Mealie image has no wget/curl — use Python TCP socket check for healthcheck; set `start_period: 60s` for DB migration time
8. Always verify container images have the healthcheck tool (`wget`, `curl`, etc.) before using it — Alpine has BusyBox wget, Python images have `python3`

## End-of-session checklist

Before ending a session, always:

1. **Commit and push** all code changes
2. **Build, push, and deploy** the new controller image (if controller code changed)
3. **Update CONTEXT.md** with what was done, decisions made, and what's next
4. **Update controller/README.md** if architecture or features changed
5. **Verify** the deployment is working (check `docker ps` and logs)