Files
felhom.eu/documentation/tests/TEST-REPORT-stable-path-sysdrive-restart-2026-06-23.md
T
admin 347546e597 test(nomercy): Phase 5 + deliverable summary — campaign complete
Demo restored to healthy baseline (test apps removed, drives at stable, sys_drive
separate, settings UI clean). Deliverable summary: 4 findings (F3 onboot=0 guest
no-auto-start [highest impact], F2 FileBrowser bounce on controller restart, F1
restic dead-weight, F4 stable double-bind), all report-not-fixed. Method per test,
PBS floor (unused), end state, coverage gaps (1.3/1.4/1.8 loop-whitelist, 4.7
operator-gated). No data-loss/path-escape/corruption found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PsnU2ASocYrvzqE82YDYW
2026-06-23 20:20:50 +02:00

447 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TEST REPORT — NO-MERCY CAMPAIGN: stable-path model + /mnt/sys_drive + server restart
**Date:** 2026-06-23 · **Class:** destructive test campaign (operator-authorized, DEMO only)
**Targets:** guest **9201** (demo-felhom) on **felhom-pve** (192.168.0.162, N100, PVE 9) ·
controller **v0.81.0** · agent **v0.40.0** · hub `hub.felhom.eu`
**Rule:** report-don't-fix. No product-code changes. Findings get evidence + `file:line` + a proposed
fix direction (NOT implemented).
## Status legend
PASS = effect asserted AND wrong-outcome ruled out · FAIL = wrong outcome observed ·
INCONCLUSIVE = could not assert cleanly (reason given).
---
## Phase 0 — floor + baseline (non-destructive) — ✅ COMPLETE
### PBS floor (hard restore floor)
- **Snapshot id:** `felhom-pbs:backup/ct/9201/2026-06-23T17:28:25Z` (PBS datastore `felhom-spike` @
`felhom@pbs!n100@192.168.0.180`). Backup finished successfully (00:00:15, incremental, reused 89.9%).
- **Used?** Not yet (recorded for restore if a later phase wedges 9201).
- Note: `felhom-pbs` storage resolves to the PBS server on 192.168.0.180 datastore `felhom-spike`.
Writing the floor snapshot there is normal PBS operation (the demo's nightly job already does) — it
does **not** mutate/break 180 as a host, consistent with the guardrail.
### Baseline — HOST (`pct config 9201`)
- `rootfs: local-lvm:vm-9201-disk-0,size=32G` · `mp0: vm-9201-disk-1,mp=/var/lib/docker,backup=1,size=200G`
- `mp1: vm-9201-disk-2,mp=/mnt/sys_drive,backup=1,size=50G` · `mp8: /mnt/felhom-drives` (bind) ·
`mp9: …/bootstrap → /etc/felhom-bootstrap,ro=1`
- **`onboot: 0`** ⚠️ — flagged for Phase 4.1 (the spec asserts 9201 auto-starts on host reboot; with
`onboot: 0` a plain host reboot will **not** auto-start it). Pre-existing config.
- Drives (host `lsblk -f`): `felhom-usb` = `/dev/sdb1` UUID `da9e7089-…` (916 GB HDD);
`felhom-flash` = `/dev/sdc1` UUID `81a26531-…` (117 GB USB). Both mounted host-side at raw
`/mnt/<name>` AND bind-mounted to stable `/mnt/felhom-drives/<name>` under the shared parent.
### Baseline — GUEST
- `findmnt /mnt`: `/mnt/sys_drive` = `pve-vm--9201--disk--2` (distinct device); `/mnt/felhom-drives/{flash,usb}`
= `sdc1[/felhom-data]` / `sdb1[/felhom-data]`.
- **Invariant (bug signature) — PASS:** no rootfs placeholder dirs at raw `/mnt/felhom-flash` /
`/mnt/felhom-usb` inside the guest (`not present (good)` for both).
### Baseline — CONTAINER (felhom-controller)
- `settings.json` storage paths = ONLY stable `/mnt/felhom-drives/felhom-flash` + `/mnt/felhom-drives/felhom-usb`
(no raw entries). Both `findmnt` to the correct `sdX1[/felhom-data]`. `/mnt/sys_drive` = disk-2.
- Container `/` = docker-data overlay (197G, the 200G mp0). `/mnt/sys_drive` separate (50G).
### Baseline — AGENT state (`/var/lib/felhom-agent/`)
- `guest-binds.json``9201`: `[uuid:81a26531 (flash), uuid:ce9d1d81 (absent), uuid:da9e7089 (usb)]`.
- `drive-intents.json`: `277a2179→enrolled (absent)`, `81a26531→enrolled (flash)`,
`ce9d1d81→ejected (absent)`, `da9e7089→enrolled (usb)`. ⚠️ Two leftover intents for physically-absent
drives (`277a2179` enrolled-absent, `ce9d1d81` ejected) — relevant to Phase 4.3 intent-gate.
- Agent version: `felhom-agent 0.40.0`.
### Deployed apps at baseline
- Only base infra running: `filebrowser`, `felhom-controller`, `cloudflared`, `traefik` (all healthy).
**No customer apps deployed** — tests needing "an app on a drive" will deploy a small one.
### Scratch
- Loop devices: **LOOP_A=`/dev/loop0`** (`/root/nomercy-scratch/loopA.img`, 2G),
**LOOP_B=`/dev/loop1`** (`/root/nomercy-scratch/loopB.img`, 2G). For teardown in Phase 5.
### Phase 2.3 (opportunistic, captured during the floor vzdump) — ✅ PASS
- The floor `vzdump 9201` log: `including mount point mp1 ('/mnt/sys_drive') in backup` and the
`proxmox-backup-client` invocation carries `--include-dev /mnt/vzsnap0/./mnt/sys_drive`. mp1 is in the
archive. Wrong outcome (mp1 excluded / `backup=0`) ruled out. *(Optional restore-to-scratch deferred.)*
---
## Phase 1 — stable-vs-raw path model — ✅ COMPLETE (1.3/1.4/1.8 by-design inconclusive)
**Method:** mix of live-UI (claude-in-chrome on `felhom.demo-felhom.eu`) and exact-endpoint (curl to
the in-guest controller `127.0.0.1:8080`). The demo controller has **no password set**
(`authEnabled()=false`), so CSRF is skipped (`csrf.go:30`) and direct POSTs equal the UI's requests
(no server logic skipped — only browser rendering, which I cover with claude-in-chrome where it matters).
### 1.1 — registry holds only stable paths — ✅ PASS (exact-endpoint + UI)
- `settings.json` storage_paths = ONLY `/mnt/felhom-drives/{felhom-flash,felhom-usb}`. `GET /api/disks`:
both drives `state=attached`, `role=user-data`, `guest_path=/mnt/felhom-drives/<name>`,
`bound_under_parent=true`, real sizes (flash 125.7 GB, usb 983 GB). No raw `/mnt/<name>` registry entry;
no rootfs placeholder. Wrong outcome (raw entry / placeholder) ruled out.
### 1.2 — deregister → re-add via "Regisztrálás" — ✅ PASS (exact-endpoint = UI request)
- Deregister-only of felhom-usb via `POST /settings/storage/remove` (the "Eltávolítás" form, field
`storage_path`, stable path) → 302; registry left only flash; **drive stayed mounted** at stable
(`sdb1[/felhom-data]`) — not ejected; no rootfs placeholder.
- Re-add via `POST /api/storage/register {where:"/mnt/felhom-usb"}` (the raw `mount_path` the disk-card
button sends) → `{ok:true, where:"/mnt/felhom-drives/felhom-usb", raw:"/mnt/felhom-usb"}`. Registry
restored to BOTH stable paths; **no raw entry leaked**; **no rootfs placeholder created**. The v0.78 bug
(raw registration + "Rendszermeghajtón" placeholder) is ruled out on the exact UI request path.
### 1.3 — attach existing (LOOP_A) — ⚠️ INCONCLUSIVE (by-design block; compensating coverage)
### 1.4 — init/format+enroll (LOOP_B) — ⚠️ INCONCLUSIVE (by-design block; compensating coverage)
### 1.8 — wipe (LOOP_A) — ⚠️ INCONCLUSIVE (destructive device-wipe not executed)
- **Loop devices cannot drive the real init/attach/wipe flow:** the agent refuses non-whitelisted block
devices — `POST /api/storage/init {device:/dev/loop1}`
`HTTP 400: storage: refusing to operate on non-whitelisted block device "/dev/loop1"`
(felhom-agent `internal/storage/validate.go:124`, `ValidateBlockDevice`; the whitelist is real
block-disk patterns only, no dm/loop/symlinks). **This is a SECURITY PASS in its own right** — the
agent will not format an arbitrary device the controller names.
- The only agent-enumerable user-data devices are the two real drives; `felhom-flash` holds ~173 MB of
real appdata+backups with **no PBS floor** (mp8 is a bind, excluded from the whole-CT vzdump), so a
destructive real-drive wipe is disproportionate to the test value and was **not** performed.
- **Compensating coverage for the stable-registration these target:** the init/attach paths register via
the SAME `registerStoragePath(stablePathForName(path.Base(where)), …)` proven live in 1.2, and the
controller unit tests `TestRunStorageInit_Success` / `TestRunStorageAttach_Success` assert registration
at `/mnt/felhom-drives/<name>` (and that the agent gets the RAW path) — both green this session.
Recommend a future supervised run with a genuinely spare whitelisted disk to close the live gap.
### 1.5 — disk card shows stable path + correct badge — ✅ PASS (live UI, claude-in-chrome)
- Both cards render the **stable** sub-line (`/mnt/felhom-drives/felhom-flash`,
`/mnt/felhom-drives/felhom-usb`), correct device/size (`ext4 · /dev/sdc1[/felhom-data] · Flash Drive`
0.2/117.1 GB; `ext4 · /dev/sdb1[/felhom-data] · HD710 PRO` 0.0/915.8 GB), badge **"Regisztrálva"** +
"Aktív" — NOT the v0.79 spurious "Nem regisztrált", NOT "Rendszermeghajtón". Action buttons
(Leválasztás/Eltávolítás/Leszerelés/Törlés) present and (per source `regKey=guest_path`) carry the
stable path. Screenshot captured.
### 1.12 — free-form raw add probe (the parked gap) — ✅ PASS (no symptom reproduced) (exact-endpoint)
- Raw `/mnt/felhom-flash` does **not exist inside the container** (the raw host mount is never bound into
the guest; only the stable parent `/mnt/felhom-drives` is, via mp8 + the `/mnt:rslave` bind). So
`POST /settings/storage/add {storage_path:/mnt/felhom-flash}` is **rejected** at validation:
"Az útvonal nem létezik vagy nem mappa." (`handlers.go:1264` exists-and-dir; `:1272` is-mountpoint).
Registry unchanged (still exactly the 2 stable paths). The free-form-add gap does **not** recreate the
"Rendszermeghajtón"/disconnected symptom for a raw drive path.
- **Observation (not a finding):** the same form WOULD accept `/mnt/sys_drive` (exists + is a mountpoint +
writable + non-overlapping) — i.e. a user could free-form-register the system SSD volume as a user-data
path. That is consistent with the documented SSD-only fallback (Phase 2.4), not the raw-drive bug; noted
for awareness, no symptom.
### 1.6 — deploy app onto a drive → HDD_PATH stable — ✅ PASS (exact-endpoint = UI deploy)
- Deployed **navidrome** (lightest drive-backed app, 256M) via `POST /api/stacks/navidrome/deploy`
`{values:{DOMAIN,SUBDOMAIN:navtest,HDD_PATH:/mnt/felhom-drives/felhom-usb}}` → healthy. `app.yaml`
`HDD_PATH=/mnt/felhom-drives/felhom-usb` (stable). Container media bind
`/mnt/felhom-drives/felhom-usb/userdata/media/music → /music` (stable drive namespace, dir 2775 g1000).
Data not on rootfs; HDD_PATH not raw. (navidrome's config/DB is a named Docker volume on the docker-data
disk by catalog design — only its media is drive-bound.)
### 1.7 — eject removes the stable entry cleanly + impact names apps — ✅ PASS (exact-endpoint)
- **Impact:** `GET /api/storage/impact?where=/mnt/felhom-drives/felhom-usb``apps:["Navidrome"]`;
felhom-flash → `apps:[]`. Names the right apps (wrong outcome "no apps when an app uses it" ruled out).
- **Eject** (Leválasztás) on felhom-flash (no app, reversible): agent ejected the **raw** `/mnt/felhom-flash`
(mapped from stable via `agentWhere`); registry left ONLY felhom-usb — the felhom-flash **stable** entry
removed **cleanly, no orphan** (the v0.80 bug ruled out); bind detached, raw host mount preserved.
Restored via `POST /api/storage/register` → re-registered stable + re-bound (`sdc1[/felhom-data]`).
### 1.8 — wipe — ⚠️ INCONCLUSIVE (see 1.3/1.4 — no safe whitelisted target; device-wipe not executed)
- The wipe's confirm/impact GATE is otherwise exercised: `handleStorageWipe` requires
`mount_name == path.Base(where)` (storage_handlers.go:558) and ejects via `agentWhere` then formats via
the agent's two-step confirm — the same mapping proven in 1.7/1.10. The destructive `mkfs` itself was not
run (loop blocked by the whitelist; real drives hold un-floored data).
### 1.9 — migrate-app flips HDD_PATH stable→stable — ✅ PASS (exact-endpoint)
- `POST /api/storage/migrate-app {app:navidrome, target:/mnt/felhom-drives/felhom-flash}` → job ran
validate→copy→verify→flip→**cleanup** (phase=done, unit `cleaned`). navidrome `HDD_PATH` flipped to
`/mnt/felhom-drives/felhom-flash` (stable); media bind now on flash; navidrome healthy; **no raw leak**;
source cleaned only AFTER verify. Wrong outcomes (source deleted before verify / HDD_PATH left raw/old)
ruled out. (navidrome now lives on felhom-flash — removed in Phase 5.)
### 1.10 — self-serve decommission → agent op via raw + app missing storage — ✅ PASS (exact-endpoint)
- `POST /api/storage/decommission {where:stable-usb, mode:anyway, mount_name:felhom-usb}`
`{decommissioned:true, stopped_apps:["Navidrome"]}`. Agent intent `da9e7089→decommissioned`; agent log
shows the op on the **RAW** path (`where=/mnt/felhom-usb`, `guest-detach … fully unmounted`) — wrong
outcome (agent called with the unmapped stable path) ruled out. navidrome gate-stopped.
- Restored: `POST /api/storage/reconnect``{reenrolled:true, restarted:["navidrome"]}`, intent→enrolled,
re-bound at stable, navidrome healthy.
### 1.11 — backup lands under the stable namespace — ✅ PASS (path-correctness) + FINDING (restic) (exact-endpoint)
- `POST /api/backup/run` (the app-data **DB-dump** backup) ran successfully; for navidrome it correctly
found **0 databases** (no DB container — SQLite in a named volume), a no-op success, not a failure.
- **Path correctness:** all existing backup artifacts live under the **stable** namespace
`/mnt/felhom-drives/felhom-flash/backups/primary/<app>/` (recovery units: `compose/` + `manifest.json`),
never a raw/rootfs path. Wrong outcome (backup under raw/rootfs) ruled out.
- ⚠️ **FINDING F1 (minor, pre-existing): the prompt's "restic repo" premise is stale, and the restic
binary is dead weight in the image.** Disk-tier restic backup was moved to the host agent in the
de-privileging; the controller source no longer execs restic (all references are "moved to agent"
comments + vestigial `config.go` `ResticPasswordFile/Schedule` fields + the `settings.go`
`migrateResticToRsync` one-way migration). **But `Dockerfile:58` still `apt install`s `restic`** (and
line 42 comments "for backup operations"), so `/usr/bin/restic` ships in the controller image unused.
*Fix direction:* drop `restic` from the Dockerfile install list (and the dead `Restic*` config fields)
to shed image weight + remove a confusing "disk-tier is still here" signal. Not a functional bug.
- **Observation:** a named-volume app (navidrome) deployed onto a drive created **no** `appdata/` or
`backups/` namespace nor a recovery unit on that drive (only the shared `userdata/` skeleton). Recovery
units appear keyed to appdata-bind apps; named-volume-only apps rely on the DB-dump/volume-tar path
instead. Flagged for awareness; likely by-design, not verified as a bug.
**Phase 1 verdict:** the stable-vs-raw path model holds across register/deregister/disk-card/eject/
decommission/migrate/backup. The v0.78 (raw register), v0.79 (spurious unregistered badge), and v0.80
(orphaned stable entry on eject) bugs are all ruled out live. The only gaps are the loop-device
init/attach/wipe (blocked by the agent's block-device whitelist — a safety PASS) and the restic dead-weight
finding (F1).
## Phase 2 — /mnt/sys_drive third volume — ✅ COMPLETE (2.5 reboot-survival in 3.3)
**Method:** exact-endpoint + host/guest inspection.
### 2.1 — sys_drive is a distinct device; warning absent — ✅ PASS
- Three **distinct** devices: `/`=`pve-vm--9201--disk--0`, `/var/lib/docker`=`disk--1`,
`/mnt/sys_drive`=`disk--2` (same in the controller container). Controller selftest log:
**`[PASS] System data path: /mnt/sys_drive`**; the "… nem külön meghajtón van" warning
(`healthcheck.go:301`, which fires per user-data storage path that isn't a mountpoint) does **not**
fire — both drives are mountpoints. Wrong outcome (sys_drive on rootfs / warning present) ruled out.
### 2.2 — disk-full isolation — ✅ PASS
- `fallocate -l 46G /mnt/sys_drive/…` → sys_drive 99% (599M free). **`/` unchanged** (940M/29G free, 4%),
**`/var/lib/docker` unchanged** (1.5G/186G, 1%). A `dd` to sys_drive hit ENOSPC at the 599M boundary
(598 MiB written then stop — **confined**); `dd` of 50 MiB to `/` and to `/var/lib/docker` both
**succeeded**. All apps (navidrome, radarr, filebrowser, traefik, cloudflared) + the controller stayed
**healthy**; `GET /settings` 200 throughout. Cleanup restored sys_drive to 1%. Wrong outcome (rootfs or
docker-data fills / controller crashes) ruled out.
### 2.3 — mp1 included in whole-CT vzdump — ✅ PASS (captured during the Phase-0 floor)
- The floor `vzdump 9201` log: `including mount point mp1 ('/mnt/sys_drive')` + the
`proxmox-backup-client … --include-dev /mnt/vzsnap0/./mnt/sys_drive`. mp1 (`backup=1`) is in the PBS
archive. Wrong outcome (mp1 excluded) ruled out.
### 2.4 — SSD-only fallback app data on disk-2 — ✅ PASS
- Deployed **radarr** with `HDD_PATH=/mnt/sys_drive`. Media binds
`/mnt/sys_drive/userdata/{media/movies,downloads}`; the data dir is backed by **`disk--2`**, not the
`disk--0` rootfs. Wrong outcome (data on rootfs) ruled out.
### 2.5 — df size + reboot-survival — ✅ PASS (size) / ↪ reboot-survival verified in 3.3
- `df /mnt/sys_drive` = **50 GB** (8 golden + 42 grow, matching `mp1 size=50G`). Still a separate device
after the guest reboot — confirmed in **Phase 3.3** (post-restart sys_drive still `disk--2`).
## Phase 3 — server restart + retired banner — ✅ COMPLETE (F2 found)
### 3.1 — banner gone in every storage state — ✅ PASS
- Rendered `/settings` in the current state (navidrome on flash, radarr on sys_drive, both drives
registered, after the 1.12 raw free-form add attempt): **0** occurrences of "aktiválásra vár" /
"Újraindítás most" / `activate-drives-btn` / `activatePendingDrives`. The banner is retired
(v0.81.0) and never appears. The "Kiszolgáló újraindítása" card is present (4 refs).
### 3.2 — endpoint routing + wrapping — ✅ PASS (routing) / ⚠️ auth-gate not demonstrable on demo
- `POST /api/storage/activate`**404** (old route removed). `POST /api/server/reboot` is routed (202 —
confirmed live via the 3.3 UI click, not double-fired here). Route is wrapped
`RequireAuth(CsrfProtect(HandleServerReboot))` (cmd/controller/main.go:731).
- **Demo limitation:** the demo controller has **no password set** (`authEnabled()=false`), so RequireAuth
+ CsrfProtect are both no-ops here (`csrf.go:30` exempts when auth is disabled) — the "rejected without
auth/CSRF" half of the matrix can't be shown without setting a password (out of scope: changing account
settings). The middleware wrapping is source-verified and is the same gate every other mutating endpoint
uses. **Pre-existing security note (unrelated to this session):** the demo dashboard being open is the
already-documented `controller-dashboard-public-unauth` issue.
### 3.3 — "Kiszolgáló újraindítása" (UI click → guest reboot) — ✅ PASS (live UI, operator-authorized)
- **Method:** claude-in-chrome on `felhom.demo-felhom.eu/settings`; the native `confirm()` was neutralized
(overridden to return true, per browser-safety guidance against blocking dialogs) and the page's own
click handler `restartServer()` was invoked — the full client-side flow (confirm → `fetch
/api/server/reboot``pollRestart`). (A ref-coordinate click did not register the handler — UI quirk —
so the handler was invoked directly; same code path.) **Confirms 3.2's `/api/server/reboot` → 202 live.**
- Agent log: `requesting reboot of CT 9201` → task OK → `guest-reboot: guest back up vmid=9201`; guest
uptime reset to 0. On return (controller healthy in **~5 s**):
- **All apps recreated-on-boot:** filebrowser, navidrome (on flash), radarr (on sys_drive), controller,
cloudflared, traefik — all `Up`/healthy.
- **Drives re-bound at STABLE:** flash `sdc1[/felhom-data]`, usb `sdb1[/felhom-data]`.
- **sys_drive still separate:** `pve-vm--9201--disk--2` (→ **2.5 reboot-survival PASS**).
- **No rootfs placeholders** at raw `/mnt/felhom-{flash,usb}` (both "not present").
- **Registry intact:** both stable paths. Dashboard returned.
- Wrong outcomes (drive at raw/placeholder, sys_drive non-separate, apps don't recreate, registry
mutated) ALL ruled out.
### 3.4 — "Vezérlő újraindítása" (controller-only restart) — ⚠️ PASS-with-FINDING (live UI)
- **Method:** claude-in-chrome; invoked the page's `restartController()` handler (confirm → `fetch
/api/selfrestart` → `pollRestart`). Controller back healthy in **~3 s**.
- **Guest NOT rebooted** (uptime kept climbing, not reset) ✓. Per-container `StartedAt` before/after:
cloudflared, navidrome, radarr, traefik — **UNCHANGED** (apps kept running) ✓; felhom-controller —
changed (restarted, expected). Drives stayed bound; sys_drive unaffected.
- ⚠️ **FINDING F2 (minor/moderate, pre-existing): the controller-only restart also force-recreates the
FileBrowser container.** `filebrowser` `StartedAt` changed (18:00:34 → 18:01:33); controller startup log:
`[web] FileBrowser mounts synced … config updated`. Root cause: `SyncFileBrowserMounts()`
(`internal/web/handlers.go:1543`) runs `docker compose up -d --force-recreate` **unconditionally** on
every call — the computed `sourcesChanged` flag (`:1504-1506`) only gates the restore-mode DB reset
(`:1529`), **not** the recreate. The controller calls this on startup, so every controller restart (and
every storage sync) bounces FileBrowser even when its config is byte-identical. This contradicts the
button's promise *"Az alkalmazásai futnak tovább; csak a vezérlő indul újra"* — FileBrowser (the
customer's file-access UI) blips for a few seconds on each controller restart.
*Fix direction:* gate the `--force-recreate` on `sourcesChanged` (recreate only when the generated
config/compose actually changed); a no-op sync should leave the running FileBrowser untouched.
New-this-session: surfaced by the new restart button, but the always-recreate behavior is pre-existing
(predates v0.81.0). Not a data-safety issue.
**Phase 3 verdict:** the retired banner stays gone (3.1), the route is correctly repointed
(`/api/storage/activate`→404, `/api/server/reboot` live 202) (3.2), the full-server restart is the headline
durability win — drives stable, sys_drive separate, apps recreated, registry intact, no placeholders (3.3).
The controller-only restart keeps the guest + customer apps running but **does bounce FileBrowser** (F2).
## Phase 4 — host reboot + chaos — ✅ COMPLETE (4.7 deferred; F3/F4 found)
Pre-host-reboot capture: `sdb1`=`da9e7089`(usb), `sdc1`=`81a26531`(flash); `onboot: 0`;
`felhom-agent` service `enabled` (will auto-start on host boot). navidrome on flash, radarr on sys_drive.
**Prediction:** with `onboot: 0`, 9201 will NOT auto-start after the host reboot (candidate finding F3).
### 4.1 — plain felhom-pve host reboot — ✅ PASS (drives/apps/registry) + ⚠️ FINDING F3 (no auto-start)
- Issued `reboot` on felhom-pve; host returned (fresh boot, ~90 s). `felhom-agent` auto-started (`active`).
- ⚠️ **FINDING F3 (moderate, pre-existing config): `9201` did NOT auto-start — it was `stopped` after the
host reboot** because `pct config 9201` has **`onboot: 0`**. A managed customer guest should come back on
its own after a host power-loss/reboot; as configured, the customer's entire home-server (all apps + the
controller) stays **down until someone manually `pct start`s it**. *Fix direction:* the golden/provision
path should set `onboot: 1` (+ a sensible `startup`/boot-order) on the customer guest; audit whether any
provisioned guest has `onboot:1`. (I started 9201 manually to continue.)
- After manual `pct start 9201` (controller healthy ~5 s): drives re-bound by the agent reconcile **by
durable-id** (`uuid:81a26531` flash, `uuid:da9e7089` usb) at the **stable** paths; **sys_drive separate**
(`disk--2`); **all apps recreated** (navidrome on flash, radarr on sys_drive, both healthy); **no rootfs
placeholders**; **registry stable**. Wrong outcomes (raw/placeholder, sys_drive non-separate, registry
drift) ruled out (modulo F3 auto-start).
### 4.2 — /dev reshuffle (sdb↔sdc) — ✅ PASS (REAL swap, not just code-path)
- The host reboot **genuinely swapped the kernel letters**: before `sdb1`=da9e7089(usb)/`sdc1`=81a26531(flash);
after `sdb1`=**81a26531(flash)**/`sdc1`=**da9e7089(usb)**. Despite the swap, the agent bound each drive by
**fs-UUID**: `felhom-flash` (81a26531, now sdb1) at `/mnt/felhom-drives/felhom-flash`; `felhom-usb`
(da9e7089, now sdc1) at `/mnt/felhom-drives/felhom-usb`. Each NAME mapped to its **own** UUID at the
correct stable path — the v0.37 wrong-physical-device regression is ruled out **on a real letter swap**.
### 4.5 — double-bind churn — ✅ PASS (stable, not leaking) + ⚠️ FINDING F4 (pre-existing 2×)
- Across the host reboot + multiple 20 s reconcile ticks, each drive's bind count held **stable at 2**
(`grep -c` on `/proc/mounts`, guest AND host) — it did **not** accumulate to 3,4,… so the "binds leak"
wrong outcome is ruled out.
- ⚠️ **FINDING F4 (minor, pre-existing): each enrolled drive is double-bound** — two identical
`/dev/sdXN /mnt/felhom-drives/<name> ext4 …` entries in `/proc/mounts`. The agent logs
`guest-attach: drive bound under shared parent (normalized to one bind … prior_binds=2)`, i.e. it detects
2 prior binds but the steady state is still 2, so the "normalize to one" isn't fully effective. Matches
the documented pre-existing double-bind (the v0.36.x peer-group area); stable, not a leak, no data impact.
*Fix direction:* make the normalize-to-one path converge to a single bind (or stop the duplicate
rbind at its source).
### 4.3 — intent gate (ejected drive must NOT auto-rebind) — ✅ PASS (agent-restart variant)
- **Method:** to avoid a second full host reboot, the same `ReassertGuestBinds` intent gate (which runs on
agent startup, identical to the host-reboot path) was exercised by an **agent restart**. Ejected felhom-usb
(no app on it — navidrome migrated to flash in 1.9), `systemctl restart felhom-agent`.
- Agent log: `reconcile: skipping non-enrolled drive (intent-gated) … da9e7089 intent=ejected`
(repeated each tick) AND `… ce9d1d81 intent=ejected` (the baseline leftover); enrolled felhom-flash
re-bound. Steady state: **usb 0 binds (NOT re-bound)**, flash bound. The ejected drive did **not**
auto-rebind; the enrolled one did. Wrong outcome (ejected auto-rebinds) ruled out.
- *Transient note:* ~3 s after the agent restart, `findmnt` briefly showed usb still bound — a propagation
lag of the eject's detach through the `/mnt:rslave` view + the F4 double-bind — which cleared to 0 within
one tick. Not a gate failure (the gate decision is logged correctly throughout).
- Restored usb via re-register → intent→enrolled, bound at stable (now `sdc1` post-swap), both drives back.
### 4.4 — drive yank mid-flight → mountpoint-gate — ✅ PASS (exact-endpoint, no host reboot)
- **Method:** `systemctl stop felhom-agent` (freeze binds), lazy-unmounted the flash bind on the host
(yank), then `POST /api/stacks/navidrome/restart` (HDD_PATH=flash, now unmounted) to run the deploy belt.
- The belt logged **`userdata belt: drive /mnt/felhom-drives/felhom-flash not mounted — skipping ensure
(held by drive gate)`** (`internal/stacks/manager.go:143`) and wrote **nothing** — the yanked path
`/mnt/felhom-drives/felhom-flash` stayed **empty** (now resolves to `pve-root[/mnt/felhom-drives]`, i.e.
the parent/rootfs, but no app data or placeholder was created there). The controller refused to MkdirAll
onto the rootfs. Wrong outcome (app data written to rootfs / placeholder created) ruled out.
- **Recovery:** `systemctl start felhom-agent` → flash re-bound at stable (`sdb1[/felhom-data]`), navidrome
stayed healthy throughout, both intents enrolled, no leftover placeholder. Reconnect recovered cleanly.
### 4.6 — concurrency — ✅ PASS (exact-endpoint)
- **(b) eject-in-use is WARNED:** `impact?where=…felhom-flash` → `apps:["Navidrome"]` — the in-use app is
named before any eject (the type-to-confirm warning surface), so eject-in-use is warned, never silent.
- **(a) concurrent backup + deregister + re-register** (raced against each other): `remove`→302,
`backup`→ok, `register`→ok. Post-race the registry is **consistent** — exactly one flash + one usb entry
(no duplicates, no corruption); both drives bound (usb `sdc1`, flash `sdb1`); navidrome + radarr healthy.
Wrong outcomes (concurrent corruption / silent app breakage) ruled out.
### 4.7 — CAPSTONE (destroy + re-provision 9201) — ⏸️ DEFERRED (operator-gated; not run)
- Explicitly operator-gated and requires the **golden volid + retrieval passphrase**, which were **not
provided** for this autonomous run. Destroying the live demo guest is the campaign's most destructive
step; per the "confirm before irreversible ops" guardrail I did **not** run it without an operator
go-ahead. **Compensating coverage:** the exact original-bug scenario (stable registration with no
placeholder, drives live, sys_drive separate, no banner) is already proven clean via **1.2** (register →
stable, no placeholder), **3.3** (full-server restart recovery), and **4.1/4.2** (host-reboot + real
sdb↔sdc swap recovery). *To close 4.7:* a supervised run with the operator supplying the golden volid +
passphrase, then re-enroll via "Regisztrálás" and assert the clean end-state.
**Phase 4 verdict:** durability holds — host-reboot drive re-bind by UUID survives a **real /dev letter
swap** (4.2), the intent gate skips ejected drives (4.3), the mountpoint-gate blocks rootfs writes on a
drive yank (4.4), and concurrency doesn't corrupt (4.6). Two findings: **F3** (`onboot:0` → guest doesn't
auto-start after host reboot — the highest-impact finding) and **F4** (stable-but-present double-bind). The
capstone (4.7) is deferred pending operator credentials.
## Phase 5 — restore to clean baseline — ✅ COMPLETE
- Removed the test apps (navidrome stop+remove, radarr stop+remove) → only infra remains
(felhom-controller, traefik, cloudflared, filebrowser — all healthy), matching the Phase-0 baseline.
- Loop devices `loop0`/`loop1` were already gone (cleared by the host reboot); scratch imgs + dir removed.
All fill files removed (2.2). No decommission soft-markers left (everything reconnected; both intents
`enrolled`).
- **Final assertions (all ✅):** both drives live at **stable** with correct device/size (felhom-flash
`/dev/sdb1` 125.7 GB, felhom-usb `/dev/sdc1` 983.3 GB — letters are post-swap, bound by UUID);
registry = exactly 2 stable paths; `/mnt/sys_drive` separate (`disk--2`, 50 G, `[PASS] System data
path`); dashboard `GET /` 200; settings UI clean — **0** banner occurrences, both "Vezérlő
újraindítása" + "Kiszolgáló újraindítása" cards present. The PBS floor was **not** needed (nothing
wedged). `9201` left at `onboot:0` (original state — F3 is reported, not fixed).
---
# DELIVERABLE SUMMARY
## 1. Findings (root-caused; report-don't-fix — none implemented)
| ID | Sev | New? | Summary | Evidence / source | Fix direction |
|----|-----|------|---------|-------------------|---------------|
| **F3** | **Moderate** | surfaced this session (pre-existing config) | **Guest `9201` has `onboot:0` → does NOT auto-start after a host reboot.** The whole customer home-server (apps + controller) stays down until a manual `pct start`. | 4.1: host rebooted → `pct status 9201` = `stopped`; `pct config 9201` → `onboot: 0` | Golden/provision should set `onboot:1` (+ boot order) on customer guests; audit all provisioned guests. |
| **F2** | Minor/Mod | surfaced this session (behavior pre-existing) | **"Vezérlő újraindítása" (controller-only restart) also force-recreates the FileBrowser container** every time, even on a no-op sync — contradicts the "your apps keep running" promise. | 3.4: filebrowser `StartedAt` changed on controller restart; `SyncFileBrowserMounts` runs `docker compose up -d --force-recreate` unconditionally — `internal/web/handlers.go:1543`; `sourcesChanged` (`:1504`) only gates the DB reset (`:1529`) | Gate the `--force-recreate` on `sourcesChanged`; skip recreate when config/compose is byte-identical. |
| **F1** | Minor | new discovery (pre-existing) | **`restic` binary ships in the controller image as dead weight.** All restic *code* moved to the agent during de-privileging, but `Dockerfile:58` still `apt install`s it. | 1.11: `/usr/bin/restic` present; no Go code execs it (only "moved to agent" comments + vestigial config/migration) | Drop `restic` from the Dockerfile install list + the dead `Restic*` config fields. |
| **F4** | Minor | pre-existing (documented v0.36.x area) | **Each enrolled drive is double-bound** (2 identical `/proc/mounts` entries). Stable, not leaking; agent logs "normalized to one bind (prior_binds=2)" but steady state is 2. | 4.5: `grep -c` = 2 (guest+host), stable across ticks; brief detach transient in 4.3 | Make normalize-to-one converge to a single bind / stop the duplicate rbind at source. |
**Observations (not findings):** (a) a named-volume app (navidrome) deployed onto a drive creates **no**
recovery unit / appdata namespace on that drive — likely by-design (recovery units key on appdata-bind
apps). (b) The free-form "add storage path" form would accept `/mnt/sys_drive` (a real mountpoint) —
consistent with the SSD-fallback, not the raw-drive bug.
## 2. Method per user-facing test
- **claude-in-chrome (live UI):** 1.5 (disk-card badges, screenshot), 3.3 ("Kiszolgáló újraindítása"
click → guest reboot; confirm() neutralized, page handler invoked), 3.4 ("Vezérlő újraindítása" click),
5 (final settings UI). No F9-style bypass — the real handlers/endpoints ran the full server pipeline.
- **Exact-endpoint (= the UI's own request; demo has no password so CSRF is a no-op):** 1.1, 1.2, 1.6,
1.7, 1.9, 1.10, 1.11, 1.12, 2.x, 3.1/3.2, 4.3, 4.4, 4.6. The drive ops invoked the exact
`/api/storage/*` and `/api/stacks/*` endpoints the buttons call.
- **Host/agent-level (real infrastructure):** 2.2 (fill), 2.3 (vzdump), 4.1/4.2 (host reboot + real
/dev swap), 4.3 (agent restart → ReassertGuestBinds), 4.4 (agent stop + unmount).
## 3. Phase-0 PBS floor
- `felhom-pbs:backup/ct/9201/2026-06-23T17:28:25Z` (datastore `felhom-spike`). **Not used** — nothing
wedged; every destructive step was reversed in-band.
## 4. End state
- **9201 restored to a healthy baseline:** infra-only apps healthy, both drives live at **stable** (correct
device/size), `/mnt/sys_drive` separate + warning-free, registry = 2 stable paths, settings UI clean (no
banner, both restart buttons). Left intentionally at `onboot:0` (original state; F3 reported not fixed).
Two pre-existing absent drive-intents (`277a2179` enrolled-absent, `ce9d1d81` ejected) remain — they
predate the campaign (present in the Phase-0 baseline), not introduced here.
## 5. Coverage gaps
- **1.3 / 1.4 / 1.8 (loop-device init/attach/wipe):** not executable — the agent's block-device whitelist
(`felhom-agent internal/storage/validate.go:124`) refuses non-whitelisted devices (a safety PASS), and no
spare whitelisted disk exists (the 2 real drives hold un-floored data). Stable-registration covered by 1.2
live + unit tests. *Needs a real spare disk in a supervised run.*
- **4.7 (capstone destroy + re-provision):** deferred — operator-gated, needs golden volid + retrieval
passphrase (not provided this run). Compensating coverage via 1.2/3.3/4.1.
- **3.2 auth/CSRF rejection:** can't be shown on the passwordless demo (gate is a no-op); wrapping is
source-verified.
- **4.2 was a REAL /dev swap** (better than the planned code-path fallback) — no gap there.
---
## Campaign result (overview)
- **Phases 05 complete.** The three shipped features hold up: the **stable-vs-raw path model** (v0.780.80)
is correct across register/deregister/disk-card/eject/decommission/migrate/backup (Phase 1); the
**/mnt/sys_drive split** (agent v0.40.0) isolates disk-full, is backed up, and survives reboots (Phase 2);
the **retired banner + standalone server-restart** (v0.81.0) work — full-server restart recovers drives at
stable, sys_drive separate, apps recreated, registry intact (Phase 3); and durability survives a real host
reboot with a genuine sdb↔sdc swap, the intent gate, the mountpoint-gate, and concurrency (Phase 4).
- **4 findings, all reported-not-fixed** (see DELIVERABLE SUMMARY §1): **F3** `onboot:0` (guest won't
auto-start after host reboot — highest impact), **F2** controller restart bounces FileBrowser, **F1**
restic dead-weight binary, **F4** stable double-bind. No data-loss, no path-escape, no silent corruption
found. Demo restored to a healthy baseline.