# REPORT — controller v0.75.0: gate userdata MkdirAll on a live mountpoint ## Baseline → target `felhom-controller` `main` v0.74.0 → **v0.75.0**. Controller-only; agent/hub untouched. ## Problem Two `MkdirAll`-into-`/userdata` sites fired without checking the drive was mounted (campaign-#2 findings #2/#3): the deploy belt (`ensureUserdataMounts`) and the FileBrowser sync. During a drive-absent window they logged `mkdir …/userdata: permission denied` + caused transient `Created` container flapping — and worse, writing into an unmounted mountpoint lands app data on the guest **rootfs**, shadowed when the drive returns (data-integrity + rootfs-fill hazard). ## Change (commit `0b2605c`) - `internal/stacks/manager.go` — `ensureUserdataMounts` skips when the `HDD_PATH` drive root is an **external** path (`!= sysDataPath`) that is **not a live mountpoint**; the app is held by `planDriveGates`. New injectable `Manager.isMountPoint` seam (defaults to `system.IsMountPoint`). The system/local path is never gated. - `internal/web/handlers.go` — the FileBrowser sync loop skips (and doesn't mount) a path under `StableParentDir` that isn't a live mountpoint, via the new pure `skipFileBrowserPath` helper. - `EnsureUserdataDir`/`EnsureUserdataSkeleton`/`planDriveGates` unchanged (gated the callers). ## Tests (green; red-proofed) `go build ./... && go vet ./... && go test ./...` all green. - T1 `TestEnsureUserdataMounts_SkipsAbsentExternalDrive` — belt skips when external drive absent. Red-proof: removing the gate → dir created → FAIL (shown, reverted). - T2 `TestEnsureUserdataMounts_EnsuresWhenMounted` — belt ensures when drive mounted. - T3 `TestEnsureUserdataMounts_SystemPathNeverSkipped` — system/local path never gated (must-not-over-gate). - T4 `TestSkipFileBrowserPath` — only an absent external path is skipped; mounted + system paths kept. Red-proof: gate→false → absent `usb` not skipped → FAIL (shown, reverted). ## Deploy Built+pushed `gitea.dooplex.hu/admin/felhom-controller:0.75.0` on 192.168.0.180 (digest `sha256:f735676e…`; **build-server `git pull` done first** — the v0.74.0 stale-source gotcha), deployed to guest 9201 via the bootstrap path. `docker inspect`: `image=:0.75.0 running healthy`. ## LIVE acceptance — campaign-#2 C6 re-run, now clean Disconnected felhom-flash (8 apps) and observed the drive-absent window: | Check | campaign #2 (pre-fix) | **v0.75.0 (fixed)** | |---|---|---| | `mkdir …/userdata: permission denied` | many | **0** | | "could not ensure userdata skeleton" | present | **0** | | rootfs shadow dir at unmounted mountpoint | created | **none** — `/mnt/felhom-drives/felhom-flash/userdata` does not exist (bare mountpoint) | | apps during absent window | transient `Created` flapping | held cleanly (down) | | reconnect | recovered | **recovered: flash remounted /dev/sdc1, 8 apps restarted → 25 running / 0 unhealthy** | The integrity hazard (app data shadowed onto the rootfs) is closed: the now-unmounted mountpoint is empty, no `userdata/...` was written to the rootfs. ## Not touched / design note (see CONTEXT.md) The **boot-time** `mkdir … permission denied` (daemon boot-restore) is a different cause — **docker** auto-starts drive-backed containers (`restart: unless-stopped`) before the agent mounts the drives, so docker (not the belt) tries to create the bind source; `planDriveGates` recovers them after mount convergence. This task ships only the runtime gate; the boot-ordering decision is documented, not implemented.