Two record items, banked before any Go file is opened.
1. CLAUDE.md gains a standing rule beside the seam-wiring rule: a health check
issues no block I/O. A probe that touches a wedged device enters
uninterruptible sleep, survives SIGKILL, and cannot be recovered until the
device returns or the host reboots — so `systemctl restart` hangs too. A
timeout protects the caller's control flow and nothing else. Liveness is
decided from /proc and kernel state.
Measured in the R-117 spike §6.3: D state 3m50s after kill -9; a buffered
write with no fsync blocked too (O_CREAT needs journal access); statfs and
getdents returned HEALTHY on a namespace that EIOs every byte.
Repeated as a one-line pointer in felhom-agent/CLAUDE.md, because health
checks are written in that repo and felhom.eu/CLAUDE.md does not load in an
agent-only session — a standing rule that does not load where it binds is the
inert-seam shape applied to a rule.
2. The R-116 row gains the clause the spike recommended but did not apply. Its
verdict stands and every input to the pairing fix is configuration-derived.
But the over-correction window's degraded:false was read off a drive whose
bind was dead, so it evidences "the gate did not over-fire", not "the drive
was healthy". The two RETURNED lines remain a genuine positive observable, so
rule 3 is still satisfied. Nothing else about the row changed.
Nothing in the repo said which machines are safe to break. The host table gave
access and role and stopped there, so a session needing a victim had to guess --
and the guessing inverted: the two boxes that exist to be broken were treated as
sacred, and DooPlex (the recovery chain) got used because it was the only box no
spec had fenced.
New documentation/runbooks/target-selection.md -- one page, three tiers, and per
machine what is freely permitted / needs care / forbidden, each carrying its
REASON so a rule can be correctly narrowed later instead of ossifying. States the
selection rule positively (start at Tier 0; a Tier 2 box only when a task says so
explicitly; an absent fence is not permission) and that fences name ACTS, not
machines -- demo-hp's over-subscribed local-lvm is one dangerous storage, not a
dangerous box.
CLAUDE.md: host table gains a Blast radius column, gains the missing demo-hp row
(it was where the drill VMs ran and it was not in the table at all), and a pointer
line to the new runbook.
CORRECTION to the spec's problem statement: the designation was not missing. The
2026-07-25 operator ruling naming the t740 as drill+build VM host -- explicitly
"moved off DooPlex" -- already existed in operations/nodes.md. It sat where no
session reads at start, while the prohibitions were repeated in every task spec.
The defect is reachability of the ruling, not its absence, and the R-116 drill on
DooPlex contradicted a written ruling rather than filling a vacuum.
CORRECTION to the R-116 record, same commit: the baseline claimed controller
0.186.0 on both demo boxes. Only felhom-pve was sampled and generalised; demo-hp
re-checked directly runs 0.185.1, so the fleet is split and R-114's TargetAbsent
branch is absent from demo-hp. Fixed in the audit table and REPORT-r116-diag.
Docs only -- no code, no build, no deploy, no host reconfigured, no host renamed.
felhom-pve joins the tailnet as a host package (100.70.170.35, expiry
disabled); DooPlex already advertised 192.168.0.0/24 via its GitOps k3s
tailscale pod (100.107.87.53). ssh felhom-pve now targets the tailnet IP
(direct-over-LAN at home, tunnel when away); felhom-pve-lan = LAN fallback.
--accept-dns=false on the host. Host converted static->DHCP (reservation
keeps .162 at home). Measured: --accept-routes hairpins the local subnet
at home -> travel-only opt-in. PBS is offsite (own WireGuard tunnel), needs
none of this. Recorded not fixed: controller<->agent plane hard-pinned to
192.168.0.162 (agent listen_addr + guest bootstrap.json) -> not yet portable
off-LAN. No code changed. Full doc: documentation/operations/tailscale.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMForrE4c1wZxd9LukxYVt
Adds the CAMPAIGN 7 audit doc (53/53 apps deployed and validated on the demo box
through the real controller endpoints; 45 pass, 13 template fixes, ranked
findings) and the uninstall-semantics map row it proves out: remove requires stop
first, named docker volumes are ALWAYS destroyed including the DB, HDD data and
backups survive unless explicitly ticked, and the per-app offsite toggle outlives
the app while tier-2 config is cleared.
ROADMAP +3:
R-40 the update path cannot express a multi-hop major upgrade (nextcloud
forbids skipping majors; the template moved 31 -> 34)
R-41 no standing 'does every template still deploy?' check -- the sweep was
the first thing to try, and found 5 apps that had NEVER been deployable
R-42 ruling needed on DB/cache sidecar majors (deliberately not bumped)
CLAUDE.md: REPORT.md is overwritten, so a second concurrent session in this repo
writes REPORT-<topic>.md instead -- applied to this run's own report.
Campaign 7 catalog sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
- Auto-refresh: the 60s reload fires only while a live tab is active
(nav data-live-tabs="overview,applications,events,host") AND no form is
dirty (delegated input/change listener; never reset — a reload clears it).
Skipped ticks reschedule; a muted (paused) hint shows next to the toggle on
non-live tabs / dirty forms. Toggle, localStorage key, 60s cadence, and
default-on behavior unchanged. The refresh script resolves the legacy
settings→edit hash alias like the tabs script.
- Rider 4a: every template's stylesheet link is /style.css?v={{hubVersion}}
(the v0.47.0 gotcha: max-age=3600 served stale styling for up to an hour
after a deploy). Red-proof run: a reverted bare link fails the test.
- Rider 4b: CLAUDE.md standing rule — never git add -A in this repo
(the 146d165 sweep incident); explicit paths + pull-rebase + one writing
session per clone.
- Tests: Group C structural pins (attribute read, dirty listeners, alias x2,
hint element, cadence/key survivors) + Group D cache-bust over six pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZc5w5jDhFLv6qDC32KN5v
The argocd CLI on 180 has no server session and --core breaks under sudo (env stripped);
the reliable scripted sync is annotate refresh + patch .operation on the Application CR.
Verified by deploying hub v0.7.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No separate hub app; manifests/ synced by app 'felhom' (auto-sync off). Deploy =
build+push pinned image -> bump manifests/hub.yaml tag + commit -> manual sync.
Never :latest (manifest is ArgoCD's truth). Replaces the stale kubectl apply/set image steps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unify the REPORT/CHANGELOG convention with the sibling repos (REPORT.md was
append/cumulative -> now overwrite-latest; CHANGELOG stays cumulative). Reflow
removes hard mid-paragraph line wraps; rendered output unchanged. CHANGELOG entry
in hub/CHANGELOG.md. No hub code change -> no version bump.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deploy-felhom-compose -> felhom-controller, proxmox-controller -> felhom-agent in
README.md and CLAUDE.md. Hub source (templatefetcher.go) intentionally left untouched
per scope; its raw-template URL is flagged separately for the operator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add CLAUDE.md with build workflow, project overview, and key patterns
- Add .gitignore to prevent committing binaries and IDE files
- Remove hub.exe from tracking (was accidentally committed)
- Fix statusIcon: use Unicode ● character instead of HTML entities
that get double-escaped by Go html/template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>