# 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/` AND bind-mounted to stable `/mnt/felhom-drives/` 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/`, `bound_under_parent=true`, real sizes (flash 125.7 GB, usb 983 GB). No raw `/mnt/` 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/` (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//` (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/ 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 0–5 complete.** The three shipped features hold up: the **stable-vs-raw path model** (v0.78–0.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.