docs: migrate workflow to DooPlex-local execution

This commit is contained in:
2026-07-19 12:16:52 +02:00
parent 13af252907
commit ee48a48288
7 changed files with 250 additions and 67 deletions
+33 -17
View File
@@ -3,7 +3,9 @@
> Read automatically when Claude Code works in this repo. Stable orientation only — **current state
> lives in `CONTEXT.md` and the tops of `hub/CHANGELOG.md` / `scripts/CHANGELOG.md` /
> `website/CHANGELOG.md`**, never here. Cross-repo orientation (the felhom system, artifact
> taxonomy, access): workspace-root `e:\git\CLAUDE.md`; this file is `felhom.eu`-specific.
> taxonomy, access): workspace-root `/mnt/5_hdd/felhom.eu/git/CLAUDE.md`; this file is
> `felhom.eu`-specific. A versioned copy of that workspace file lives at
> `documentation/runbooks/workspace-CLAUDE.md`.
## Project overview
@@ -15,8 +17,9 @@ This repo contains:
system**: `architecture/01..05-*.md`, `proxmox-platform.md`, `tests/phase*-findings.md`,
runbooks, audits. Read these before designing.
- **Skills** (`skills/`) — the versioned source of the Claude Code skills
(`felhom-build-deploy`, `felhom-ui-design`, `felhom-testing`); install/update with
`python scripts/install_skills.py` (junction into `~/.claude/skills/`).
(`felhom-build-deploy`, `felhom-ui-design`, `felhom-testing`, `felhom-app-catalog`);
install/update with `python3 scripts/install_skills.py` (symlink into `~/.claude/skills/` on
POSIX, junction on Windows — either way repo edits are live immediately).
See `README.md` for full architecture/DNS/email/SEO docs. See `TASK.md` for the current task (if any).
See `REUSE.md` before writing new code.
@@ -65,10 +68,11 @@ pushes; **you (Claude Code) implement**. A file being open in the editor is NOT
SSH + the felhom-agent token). Mark a step HUMAN only when it genuinely needs physical presence, a
real-world decision, or credentials CC truly lacks.
- Validation of a push against a spec's criteria is project Claude's job, not yours, unless asked.
- **Browser automation available** (`claude-in-chrome`): CC can drive `hub.felhom.eu`, the website,
or any `*.demo-felhom.eu` UI for live validation + screenshots. Caveat: tools attach only to a
session STARTED AFTER the bridge connected. Note: the hub UI is operator-password-gated — CC
cannot log in; verify UI changes via render tests instead.
- **Browser automation is NOT available** in the DooPlex environment (`claude-in-chrome` was a
Windows-workstation capability). Validate at the endpoint level — invoke the exact endpoint the UI
invokes — and via render tests; say which method was used. The hub UI is operator-password-gated
anyway, so render tests were already the method for UI changes. Strict end-to-end UI coverage is a
manual click-through by the operator.
> **In every repository where you make a change, update both files in that repo:**
> - **`CHANGELOG.md`** — cumulative log, newest on top (here: per-area `hub/`, `scripts/`, `website/`).
@@ -93,14 +97,20 @@ pushes; **you (Claude Code) implement**. A file being open in the editor is NOT
- **Config:** YAML via ConfigMap at `/etc/felhom-hub/hub.yaml`. Secrets via out-of-band
`secretKeyRef` (never inline stringData — REUSE.md §3).
## SSH access
## Environment & access
Use the Windows OpenSSH binary (Git Bash's ssh fails silently): `SSH=/c/Windows/System32/OpenSSH/ssh.exe`.
Claude Code runs **on DooPlex (192.168.0.180, Debian 13, user `kisfenyo`)** — the k3s node itself.
Repos in `/mnt/5_hdd/felhom.eu/git/`, build dirs in `/mnt/5_hdd/felhom.eu/build/`. `kubectl` and the
image build/push are local commands; felhom-pve is one SSH hop.
| Host | IP | User | Role |
|------|----|------|------|
| Build server (k3s node) | 192.168.0.180 | kisfenyo | Build + push images, `sudo kubectl` |
| Demo Proxmox host | 192.168.0.162 | root@pam (SSH alias `felhom-pve`) | pveum/pct + live Proxmox validation |
| Host | Access | Role |
|------|--------|------|
| **DooPlex (this host)** | local — `/mnt/5_hdd/felhom.eu/{git,build}/` | Build + push images, `sudo kubectl` |
| Demo Proxmox host | `ssh felhom-pve` (root@192.168.0.162) | pveum/pct + live Proxmox validation |
> **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 fails
> silently). Retained in case that environment is revived.
## Build & deploy — Hub (GitOps via ArgoCD)
@@ -118,14 +128,20 @@ truth is the **manifest**:
happened — reconcile via the manifest, not the changelog.
- Green gate before any hub commit: `go build ./... && go vet ./... && go test ./...` in `hub/`.
Steps: commit+push code → `./build.sh <VER> --push` on 180 (`/mnt/5_hdd/felhom.eu/build/felhom-hub` — all felhom build dirs/repos moved off the SSD to `/mnt/5_hdd/felhom.eu/` on 2026-07-18) → bump
`manifests/hub.yaml` tag + push → ArgoCD hard-refresh + sync (kubectl-patch method in the skill) →
verify Synced/Healthy + rollout + image + startup log.
> **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).
Steps: commit+push code → `cd /mnt/5_hdd/felhom.eu/build/felhom-hub && ./build.sh <VER> --push`
(local) → bump `manifests/hub.yaml` tag + push → ArgoCD hard-refresh + sync (kubectl-patch method in
the skill, now local `sudo kubectl`) → verify Synced/Healthy + rollout + image + startup log.
## Build & deploy — Website / Manifests
- **Website** auto-deploys via git-sync; just push to `main` (live in 12 min). **Run
`python scripts/site_gates.py` after ANY website change**; new pages go into its `PAGES` list.
`python3 scripts/site_gates.py` after ANY website change**; new pages go into its `PAGES` list.
Emergency edits: https://files.felhom.eu. All `website/` HTML is **UTF-8 with BOM** — preserve it.
- **Manifests** are GitOps via the `felhom` app — commit to `main`, then deliberate sync.
+118
View File
@@ -0,0 +1,118 @@
<!-- VERSIONED COPY. The LIVE file is /mnt/5_hdd/felhom.eu/git/CLAUDE.md on DooPlex (it is not
inside any repo, so it is mirrored here to be versioned and restorable). Keep the two in
sync when either changes. -->
# CLAUDE.md — `/mnt/5_hdd/felhom.eu/git` workspace root (DooPlex)
## What this workspace is
`/mnt/5_hdd/felhom.eu/git` is a parent folder holding the felhom sibling repos. Most are one logical
product — **Felhom**, a managed home-server service for Hungarian households — spread across several
repos. (Any non-felhom repo is unrelated; ignore unless asked.)
**Claude Code runs HERE, on DooPlex (192.168.0.180), as `kisfenyo`.** Builds are local commands; the
Proxmox host is one SSH hop (`ssh felhom-pve`). The Windows workstation is no longer the
orchestration point and its trees are stale — see "Legacy: Windows workstation" at the bottom.
Run CC inside tmux so sessions survive SSH drops: **`tmux new -A -s cc`**.
## This host is production infrastructure
DooPlex runs Gitea, the container registry, k3s + Longhorn, PBS, and the hub. Treat it accordingly:
- NEVER run `docker system prune`, `docker image prune -a`, or any global Docker cleanup here.
- NEVER touch k3s data dirs, Longhorn mounts, PBS datastores, or Gitea storage. Workspace is
`/mnt/5_hdd/felhom.eu/` — stay inside it plus `~/build` symlinks/dirs.
- Destructive disk/guest operations belong to felhom-pve via the agent — never on this host.
- Do not run Claude Code with permission prompts disabled on this host.
- Watch disk headroom before large builds: `df -h /mnt/5_hdd /` — abort if either is >90%.
## The Felhom system (three-component model, Proxmox-based)
- **Hub** — operator backend on k3s (`hub.felhom.eu`). Lives in `felhom.eu/hub/`.
- **Host agent** — one per Proxmox host, operator-tier, owns all Proxmox interaction. Repo `felhom-agent/`.
- **In-guest controller** — one per customer LXC, Docker-only. Repo `felhom-controller/`.
Other felhom repos: `app-catalog-felhom.eu/` (app templates), `homelab-manifests/` (DooPlex k3s).
**Authoritative design docs (read these before designing anything):** `felhom.eu/documentation/architecture/01..05-*.md`, `felhom.eu/documentation/proxmox-platform.md`, `felhom.eu/documentation/tests/phase{0,1-2,3,4}-findings.md`.
## Per-repo guidance
When you work in a repo, read its `CLAUDE.md` (it loads on-demand the moment you touch a file there):
- `felhom-agent/CLAUDE.md` — the Go host agent.
- `felhom.eu/CLAUDE.md` — hub + website + manifests + the architecture docs.
- `felhom-controller/CLAUDE.md` — the in-guest controller.
## Skills
Four Felhom skills exist (personal scope, `~/.claude/skills/`): **`felhom-build-deploy`** (all
build/deploy/publish runbooks), **`felhom-ui-design`** (design-system v2 tokens/rules/gates),
**`felhom-testing`** (non-hollow tests + red-proofs + seams), **`felhom-app-catalog`** (catalog
authoring workflow). Source of truth: `felhom.eu/skills/`; install/update with
`python3 felhom.eu/scripts/install_skills.py` (symlink — repo edits are live immediately).
## Artifact taxonomy (READ THIS — it prevents the "what do I do?" stall)
The planning/architecture assistant (in claude.ai, "project Claude") produces files with distinct roles. **A file being open in the editor is NOT an instruction. If no task is stated, ask.**
- **`TASK.md` / `TASK-*.md`** — a spec for **you (Claude Code) to implement**. Implement it when it is placed as `TASK.md` at a repo root, or when explicitly told "implement <file>". Then push, update `CHANGELOG.md`, and write the repo's `REPORT.md`.
- **`RUNBOOK-*.md`** — an operational procedure. CC executes the steps it has access and capability for, including live validation on the demo nodes and the demo Proxmox host (CC has root@felhom-pve SSH + the felhom-agent token). A step is human-only only when it genuinely needs physical presence, a real-world decision, or credentials CC truly lacks — mark those steps HUMAN. Do not decline a whole procedure because it touches a live host or a privileged token. (Judgment still applies: confirm before irreversible ops on real customer data — but demo scratch guests are fair game.)
- **Validation/review** — checking a push against a spec's criteria is **project Claude's** job, not yours, unless asked.
## Shared conventions
- **Push to `main` directly** — no feature branches.
> **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).
> **In every repository where you make a change, update both files in that repo:**
> - **`CHANGELOG.md`** — a cumulative log of **all** changes; newest entry on top.
> - **`REPORT.md`** — **overwrite** with a summary of the **most recent** implementation (or significant validation/operational run) only; not cumulative.
>
> **Never write secrets** — tokens, passwords, private keys, API keys — into `CHANGELOG.md`, `REPORT.md`, or any committed file. Reference them as "stored out-of-band" instead.
- **Versioning** is via build-time ldflags (`-X main.version`/`-X main.Version`); bump on meaningful changes + add a CHANGELOG entry.
- Code quality: double-check for bugs/edge cases; add debug logging; **ask rather than guess** when you'd otherwise need to invent input or output.
## Live validation — no browser here
**`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.
## Access
Local (this host): repos `/mnt/5_hdd/felhom.eu/git/<repo>`, build dirs
`/mnt/5_hdd/felhom.eu/build/felhom-{controller,hub,agent}`, `sudo kubectl`, Go toolchain, Docker
build+push to `gitea.dooplex.hu/admin/`.
| Host | Access | Use |
|---|---|---|
| **DooPlex (this host)** | local — Debian 13, `kisfenyo`, `/mnt/5_hdd/felhom.eu/` | build/push images, `sudo kubectl`, build+run the agent for tests |
| Demo Proxmox host `demo-felhom` | `ssh felhom-pve` (root@192.168.0.162, no sudo) | pveum/pct + live Proxmox validation |
| Demo guest 9201 | `ssh felhom-pve "pct exec 9201 -- ..."` | the live demo controller |
| felhotest (legacy) | `ssh -p 33022 kisfenyo@router.abonet.hu` | OLD /opt/docker compose mechanism |
The demo Proxmox host key changes on reprovision (N100) → refresh with
`ssh-keygen -R 192.168.0.162` then connect with `-o StrictHostKeyChecking=accept-new`
(`ssh-keyscan` hangs — avoid it).
## Legacy: Windows workstation
Kept so the old environment can be revived; **not the current setup**.
- Repos were in `E:\git\` (`/e/git/` in Git Bash); this file lived at `E:\git\CLAUDE.md`.
- **SSH binary had to be** `SSH=/c/Windows/System32/OpenSSH/ssh.exe` — Git Bash's `/usr/bin/ssh`
lacks access to the Windows SSH Agent and fails silently. Every remote command was
`$SSH kisfenyo@192.168.0.180 "..."`; details in `felhom-controller/docs/vscode-ssh-fix.md`.
- `pct exec` over SSH needed `export MSYS_NO_PATHCONV=1` (MSYS mangled `/`-paths).
- Agent deploy was a two-hop copy: build on 180 → `scp` to the Windows box (local path needed
`cygpath -w`) → `scp` on to felhom-pve. Beware CRLF when scp-ing config files through Windows.
- Skills were installed as Windows junctions (`mklink /J`) rather than POSIX symlinks.
- `claude-in-chrome` browser automation WAS available there (attaching only to sessions started
after the bridge connected).
+16
View File
@@ -1,5 +1,21 @@
# Felhom scripts — Changelog
## install_skills.py — cross-platform (POSIX symlink / Windows junction) (2026-07-19)
Claude Code now runs on DooPlex (Debian 13), where `mklink /J` does not exist — the script would
have fallen through to COPY mode on every run, silently breaking the "repo edits are live
immediately" property that makes `felhom.eu/skills/` the source of truth.
- Link creation is now platform-dispatched behind `os.name == "nt"`: `os.symlink(...,
target_is_directory=True)` on POSIX, the existing `mklink /J` on Windows. Copy-mode fallback,
idempotency, and the re-run warning are unchanged.
- **`remove_existing()` had a real hazard on POSIX**: `os.rmdir()` fails on a symlink-to-directory,
which would have fallen through to `shutil.rmtree()` — and rmtree following a symlink into
`skills/` would have deleted the repo's own skill sources. It now unlinks symlinks explicitly
before any rmdir/rmtree path is reached. Existence checks use `os.path.lexists()` so a broken
link is seen and replaced rather than ignored.
- Human-facing output says "symlink" or "junction" per platform.
## build-felhom-iso.sh v1.22.0 — the boot screen is ours, and it offers exactly one thing (R-38) (2026-07-19)
**A boot menu is a product surface, and ours was Proxmox's.** Every ISO is now repacked after
+43 -17
View File
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
"""Install the Felhom Claude Code skills from <repo>/skills/ into ~/.claude/skills/.
Usage: python scripts/install_skills.py (run from the felhom.eu repo root or anywhere)
Usage: python3 scripts/install_skills.py (run from the felhom.eu repo root or anywhere)
Personal skills (~/.claude/skills/) apply across all projects the right scope for the 4-repo
E:\\git workspace. Preferred install is a Windows junction (mklink /J) so edits in the repo are live
immediately; if junction creation fails or isn't followed, falls back to a full COPY — in copy mode
you must RE-RUN this script after editing skills/. Idempotent: safe to re-run any time.
felhom workspace. Preferred install is a LINK so edits in the repo are live immediately: a POSIX
symlink on Linux/macOS (the DooPlex environment), a junction (mklink /J) on Windows. If the link
cannot be created or isn't followed, falls back to a full COPY — in copy mode you must RE-RUN this
script after editing skills/. Idempotent: safe to re-run any time.
"""
import os, shutil, subprocess, sys
@@ -18,7 +19,11 @@ fails = 0
copy_mode_used = False
def is_working_junction(target, src):
IS_WINDOWS = os.name == "nt"
LINK_KIND = "junction" if IS_WINDOWS else "symlink"
def is_working_link(target, src):
"""True if target resolves into src (junction/symlink already correct)."""
try:
return os.path.isfile(os.path.join(target, "SKILL.md")) and \
@@ -28,35 +33,56 @@ def is_working_junction(target, src):
def remove_existing(target):
# a junction must be removed with rmdir semantics (never recurse INTO it), a copy with rmtree
# a link must be removed WITHOUT recursing INTO it (that would delete the repo's skills);
# a real copied directory needs rmtree
if os.path.islink(target):
os.unlink(target) # POSIX symlink (also a Windows symlink-to-dir)
return
try:
os.rmdir(target) # works for junctions and empty dirs
os.rmdir(target) # works for Windows junctions and empty dirs
except OSError:
shutil.rmtree(target, ignore_errors=True)
def make_link(target, src):
"""Create the platform's live link. Returns (ok, detail-for-humans)."""
if IS_WINDOWS:
r = subprocess.run(["cmd", "/c", "mklink", "/J", target, src],
capture_output=True, text=True)
if r.returncode != 0:
return False, (r.stderr or r.stdout).strip() or "mklink failed"
return True, ""
try:
os.symlink(src, target, target_is_directory=True)
return True, ""
except OSError as e:
return False, str(e)
def install(name):
global fails, copy_mode_used
src = os.path.join(SRC, name)
target = os.path.join(DST, name)
if os.path.exists(target):
if is_working_junction(target, src):
print("OK %-22s junction (already installed, live-linked to repo)" % name)
if os.path.lexists(target):
if is_working_link(target, src):
print("OK %-22s %s (already installed, live-linked to repo)" % (name, LINK_KIND))
return
remove_existing(target)
# try junction first
r = subprocess.run(["cmd", "/c", "mklink", "/J", target, src],
capture_output=True, text=True)
if r.returncode == 0 and is_working_junction(target, src):
print("OK %-22s junction -> %s" % (name, src))
ok, detail = make_link(target, src)
if ok and is_working_link(target, src):
print("OK %-22s %s -> %s" % (name, LINK_KIND, src))
return
if not ok:
reason = detail
else:
reason = "not followed"
# fall back to copy
if os.path.exists(target):
if os.path.lexists(target):
remove_existing(target)
try:
shutil.copytree(src, target)
copy_mode_used = True
print("OK %-22s COPY (junction failed: %s)" % (name, (r.stderr or r.stdout).strip() or "not followed"))
print("OK %-22s COPY (%s failed: %s)" % (name, LINK_KIND, reason))
except OSError as e:
fails += 1
print("FAIL %-22s %s" % (name, e))
+1 -2
View File
@@ -36,9 +36,8 @@ The core rule. A guessed `wget` probe ENOENTs at runtime and the container flaps
inspection). On the demo guest:
```bash
SSH=/c/Windows/System32/OpenSSH/ssh.exe; export MSYS_NO_PATHCONV=1
for t in wget curl node python3; do
echo "$t: $($SSH felhom-pve "pct exec 9201 -- docker run --rm --entrypoint sh <image>:<tag> -c 'command -v $t'" 2>/dev/null | tr -d '\r')"
echo "$t: $(ssh felhom-pve "pct exec 9201 -- docker run --rm --entrypoint sh <image>:<tag> -c 'command -v $t'" 2>/dev/null | tr -d '\r')"
done
```
+37 -29
View File
@@ -5,24 +5,32 @@ description: Build, deploy, publish, or verify ANY Felhom artifact — felhom-co
# Felhom build & deploy runbooks
All repos live in `E:\git\` (Git Bash: `/e/git/`). Trunk-based: commit+push to `main` first, always.
**Claude Code runs on DooPlex (192.168.0.180, Debian 13, `kisfenyo`) — builds are LOCAL commands.**
All repos live under `$FELHOM_ROOT/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.)
FELHOM_ROOT=/mnt/5_hdd/felhom.eu # build-server working root — ALL felhom repos/build/drill/iso live HERE
# (moved off the SSD 2026-07-18; expands locally into the "…" SSH strings below)
FELHOM_ROOT=/mnt/5_hdd/felhom.eu # working root — ALL felhom repos/build/drill/iso live HERE
# repos $FELHOM_ROOT/git/felhom-*, build dirs $FELHOM_ROOT/build/felhom-*
```
> **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).
| Host | Access | Role |
|---|---|---|
| Build server (k3s) | `$SSH kisfenyo@192.168.0.180` | build+push images/binaries (`$FELHOM_ROOT/build/felhom-{controller,hub,agent}`, repos under `$FELHOM_ROOT/git/`), `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 |
| **DooPlex (this host, k3s)** | local | build+push images/binaries (`$FELHOM_ROOT/build/felhom-{controller,hub,agent}`, repos under `$FELHOM_ROOT/git/`), `sudo kubectl` |
| Demo Proxmox host | `ssh felhom-pve` (root@192.168.0.162) | agent deploy, `pct` into guests |
| Demo guest 9201 | via `ssh felhom-pve "pct exec 9201 -- bash -c '...'"` | the live controller |
| felhotest (legacy) | `ssh -p 33022 kisfenyo@router.abonet.hu` | OLD /opt/docker compose mechanism — not the 9201 flow |
**No browser here:** `claude-in-chrome` is not available on DooPlex. Validate at the endpoint level
(invoke the exact endpoint the UI invokes) and say which method was used.
Version bumps are ldflags-only (`-X main.version` / `-X main.Version`) — build scripts inject them; no source edit.
@@ -32,13 +40,13 @@ Version bumps are ldflags-only (`-X main.version` / `-X main.Version`) — build
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 $FELHOM_ROOT/build/felhom-controller && git -C $FELHOM_ROOT/git/felhom-controller pull && ./build.sh <VER> --push"
# 1. commit+push the repo (clean-tree gate above)
# 2. build+push image, LOCAL (build.sh does NOT git-pull — the explicit pull is load-bearing)
cd $FELHOM_ROOT/build/felhom-controller && git -C $FELHOM_ROOT/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'"
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}}'"
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`.
@@ -49,19 +57,19 @@ Runs as the NON-ROOT `felhom-agent` user: `/usr/local/bin/felhom-agent --config
(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 $FELHOM_ROOT/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/
# build LOCALLY (pull first!)
cd $FELHOM_ROOT/git/felhom-agent && git pull && go build -ldflags '-X main.version=<VER>' -o /tmp/felhom-agent-<VER> ./cmd/felhom-agent
# copy to the PVE host — ONE hop, no intermediate workstation
scp /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"
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.
patterns). After restart, check the journal for a clean `ReassertGuestBinds` and no capability-probe
degradations. (Legacy Windows workstation: the copy was two hops via the Windows box, needing
`cygpath -w` for the local scp path — and config files scp'd through Windows risked CRLF.)
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
@@ -74,21 +82,21 @@ operator-password-gated (CC cannot); flag it as an operator follow-up.
(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 $FELHOM_ROOT/build/felhom-hub && ./build.sh <VER> --push"
# 1. commit+push code 2. build+push image (LOCAL)
cd $FELHOM_ROOT/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\":{}}}}}'"
# 4. hard-refresh + sync (argocd CLI is not logged in — drive the Application CR)
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"}'
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"
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`
Push to `main` = deployed (git-sync sidecar, live in ~12 min). **Run `python3 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.
+2 -2
View File
@@ -34,8 +34,8 @@ Files: `website/assets/site.css`, `hub/internal/web/templates/style.css`,
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).
anywhere** — enforced by gates; never hunt emoji with grep (proven to false-negative multibyte on
the Windows workstation, and grep was never the sanctioned check regardless — 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` /