From 554b237ba9f3cf62b54fce47e081afd013536be1 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 15 Oct 2026 10:57:40 +0200 Subject: [PATCH] =?UTF-8?q?Media=20build=20docs:=2015=20BUILT=20+=20=C2=A7?= =?UTF-8?q?11=20(evidence=20per=20part,=20measured=20traps);=20capability?= =?UTF-8?q?=20D13;=2003=20eject=20with=20media=20apps;=2007=20per-drive=20?= =?UTF-8?q?binds=20in=20no=20tier;=2013=20the=20fresh-Plex=20boundary;=20r?= =?UTF-8?q?egister:=209=20rows=20closed=20(FIXED=20v0.313.0),=20R-1023/R-1?= =?UTF-8?q?045/R-1049=20waiting=20on=20the=20operator,=20R-1028=20narrowed?= =?UTF-8?q?,=20R-1022=20teardown=20+=20Media=20D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- .../architecture/00-capability-map.md | 1 + documentation/architecture/03-host-agent.md | 5 ++++ .../architecture/07-backup-architecture.md | 6 +++++ documentation/architecture/13-threat-model.md | 1 + .../architecture/15-media-library.md | 25 ++++++++++++++++++- .../architecture/capability-map-detail/D.md | 24 ++++++++++++++++++ documentation/backlog/CLOSED-ITEMS.md | 14 +++++++++++ documentation/backlog/OPEN-ITEMS.md | 23 ++++++----------- 8 files changed, 82 insertions(+), 17 deletions(-) diff --git a/documentation/architecture/00-capability-map.md b/documentation/architecture/00-capability-map.md index 4b17799e..ab777eee 100644 --- a/documentation/architecture/00-capability-map.md +++ b/documentation/architecture/00-capability-map.md @@ -177,6 +177,7 @@ | [D10](capability-map-detail/D.md#d10) | USB drive enrollment + unplug detection + recommission | controller, agent | **PROVEN-LIVE** | — | `audits/storage-lifecycle-acceptance-2026-06-15.md` (2026-06-15); `audits/CAMPAIGN-4-2026-07-13.md` (2026-07-13) | | [D11](capability-map-detail/D.md#d11) | Decommission (migrate-first and anyway-paths), eject | agent, controller | **PROVEN-LIVE** | — | `audits/storage-lifecycle-acceptance-2026-06-15.md` (2026-06-15) | | [D12](capability-map-detail/D.md#d12) | Boot ordering: automount + networking survive reboot; appliance self-heal watchdog | agent v0.85 | **PROVEN-LIVE** | — | `audits/CAMPAIGN-4-2026-07-13.md` (2026-07-13); `audits/CAMPAIGN-6A-2026-07-14.md` (2026-07-14) | +| [D13](capability-map-detail/D.md#d13) | Media apps see EVERY drive; a drive added, pulled, back or safely removed is handled per app (notice, real restart, wiring, torrents paused/resumed) | controller v0.313.0, catalog `0188430`, hub v0.151.0 | **PROVEN-LIVE** | Tier-0 boxes (Tester 1, demo-hp 9201); the fleet with golden 0.313.0 + the floor | `audits/media-build-2026-10-15/G/` (2026-10-15); `audits/media-drive-events-2026-10-14/` (2026-10-14) | ## E. Access, networking & household use diff --git a/documentation/architecture/03-host-agent.md b/documentation/architecture/03-host-agent.md index ebfc41a1..2c93fd80 100644 --- a/documentation/architecture/03-host-agent.md +++ b/documentation/architecture/03-host-agent.md @@ -253,6 +253,11 @@ The controller (in its LXC) reaches the agent (on the host) over the local bridg /disks/eject` (safe-unmount, **data preserved**, returns the dependent guests so the controller warns which apps lose that storage — benign), `POST /disks/format` (see the reframed principle below). The controller is Docker-only (de-privileged, slice 8C); **execution is the agent's**. + - **Eject with media apps (controller v0.313.0, R-1044, `15` §10/§11):** before the agent's `POST /disks/eject` the + controller pauses that drive's torrents and recreates every media app and the file browser WITHOUT the drive, and + afterwards KEEPS the drive registered (removed); its return is found by the agent's report — presence is a non-empty + `backing_device`, because `state` stays „attached" for a registered disk that is gone (measured on Tester 1). No + agent change was needed. **The principle (reframed for 8C):** a controller may do **non-data-destructive** storage setup **self-serve** (list, assign, eject, format a *blank* drive); **anything that can lose customer data diff --git a/documentation/architecture/07-backup-architecture.md b/documentation/architecture/07-backup-architecture.md index 14450673..cd2fdae3 100644 --- a/documentation/architecture/07-backup-architecture.md +++ b/documentation/architecture/07-backup-architecture.md @@ -332,6 +332,12 @@ Recorded here so the encryption policy is not read as covering it. → **R-108** The tiers are **inputs to recovery**, not recovery routes. §7 and §8 say what they can actually do. +**[FACT, controller v0.313.0] The per-drive media binds are in NO tier.** A media app's binds for the other drives live +in the controller-owned `docker-compose.override.yml`, which no classifier, export or removal reads (pinned by +`TestMediaLibrary_OverrideIsInNoCaptureSet`); the template's own one-drive media line stays `excluded` as before. The +family's media library is no single app's data (`15-media-library.md` §7); a household-level backup of it is a separate +decision, not made. + **[RULING 2026-09-16, operator] Tier 3 (off-site) is ON for every new customer — shared, 100 GB soft quota.** Opting a customer out stays the per-customer exception, as DR-tier-by-default has been since 2026-07-12. **The reason is this document's own [FACT] two sections down:** the whole-guest tiers do diff --git a/documentation/architecture/13-threat-model.md b/documentation/architecture/13-threat-model.md index 90bf79af..14368bfe 100644 --- a/documentation/architecture/13-threat-model.md +++ b/documentation/architecture/13-threat-model.md @@ -59,6 +59,7 @@ OS packages (exact versions, apt-signed), the installer script (by tag, R-980). |---|---|---| | end-user ↔ apps | holds, with gaps | traefik routing and header stripping hold; homepage has the Docker socket and no gate (R-981) | | **added 2026-10-14 (R-1033, P1):** a visitor ↔ Cloudflare over plain http | holds on boxes with controller ≥ 0.312.0 | before: every public name answered `http://` (login pages too), and Sonarr/Radarr/Prowlarr redirected https visitors to an absolute `http://` login; a password could cross an untrusted network unencrypted (attacker: anyone on the visitor's network). Now traefik sends every `http://` request to https (301) before an app sees it, HSTS one day; boxes below 0.312.0 (Tester 2 at 0.292.0) keep the gap until the floor delivers it. `audits/https-only-2026-10-14/` | +| **added 2026-10-15 (R-1045):** a stranger ↔ a fresh Plex | holds on controller ≥ 0.313.0; on older ones only while the household does not press „Kész" before claiming | an unclaimed Plex answers everything to a private address with no token, and through the tunnel every request comes from one. The setup gate (catalog `9a23ee5`) closes the public path; 0.313.0 opens it only when Plex itself says claimed. A status probe meant to cover older controllers OPENED an unclaimed Plex while it started (`audits/media-build-2026-10-15/G7/`) and was removed | | customer ↔ controller UI | partly | bcrypt and session hold; small gaps (R-983); family sessions survive a reset (R-976) | | controller ↔ agent | partly | every route bound to the caller's guest; a compromised controller can still flush restore points (R-973), void the recovery code (R-978), and — since agent v0.155.0 — switch the whole machine off once per power-on, holding the hub's silence alarms for up to 14 days (R-998; mitigations and residual risk: `14-host-power-off.md` §5) | | **added (R-1007):** controller → an app's own job queue | holds — nothing new | the `after_unclean_boot` commands run inside the app's container through the docker socket the controller already holds, and read the app's database with a read-only session: no key into the household's account, nothing a compromised controller could not already do (exec in any app, read its database). The household's lost-file names stay on the box (`app.yaml`, the app page); the operator event carries counts only, and a failing command's output is never logged (security review of controller `8d862e4`, fixed in `3b85a0c`) | diff --git a/documentation/architecture/15-media-library.md b/documentation/architecture/15-media-library.md index f21be798..40fcbc72 100644 --- a/documentation/architecture/15-media-library.md +++ b/documentation/architecture/15-media-library.md @@ -5,7 +5,7 @@ > learn. **No document covered this before 2026-10-14** — itself a finding: the four templates were written one by one > and never tested together (R-1014). Rulings: `09` §3 decisions 237–241. Measured on Tester 1: > `audits/media-all-drives-2026-10-14/`. Marks as in `01`: **[DESIGN]** a decision or proposal; **[FACT]** read in a -> named file or measured. **Status: DRAFT** — measured end to end on Tester 1; the layout of §2 is RULED (decision 242) and live for one drive; the drive events of §10 measured (decision 245, option A). +> named file or measured. **Status: BUILT (controller v0.313.0, 2026-10-15)** — §2–§10 are built and proven on Tester 1 and demo-hp 9201 (§11, with the evidence per part); delivery to the fleet is golden 0.313.0 → the operator's vouch → the floor (`09` §3 decision 247). Plex's real claim on the running server is proven by hand (K6-2); through the dashboard it waits on a household code (§11). ## 1. The problem @@ -228,3 +228,26 @@ WITHOUT a token — it must never be routed publicly while unclaimed; `POST /myp claims it — PROVEN 2026-10-15 with a real code (`K6-2`): 200, `claimed` 0→1, `PlexOnlineToken` written, and the open door closed (no token → 401). `ADVERTISE_IP` was not needed for the claim. So the box asks for the code only once the server is up: no 4-minute race with the pull. + +## 11. Built — controller v0.313.0 (2026-10-15) [FACT; evidence `audits/media-build-2026-10-15/`] + +| Part | What is built | Where | Proven | +|---|---|---|---| +| §2/§3 the marker | `media_library:` (`folder:`, `read_only:`) on Sonarr, Radarr, qBittorrent, Transmission, Plex, Jellyfin, Emby (catalog `0188430`); one bind per PRESENT drive in the controller-owned `docker-compose.override.yml`, rewritten before every `compose up`; absent, dead (`shutdown`) and safely removed drives never bound; network shares never | `stacks/media_library.go` | G1: every stored path, library item, watch mark and all 69 files identical after the move; G7: controller 0.312.0 ignores the marker (one drive, no override) | +| §4 restart | `up -d --force-recreate` with the stack's env; start times before/after; an error when nothing started | `stacks/manager.go` `RestartStack` | G1/G2/G3: six apps restarted in ~30 s, each start time moved | +| §6 links | qBittorrent categories + automatic management; Transmission incomplete folder off (template entrypoint + belt); Sonarr/Radarr tag, root, clients, minimum free space; Prowlarr → Sonarr/Radarr; Plex folders + scan + „scan on change" with the server's own token read at use (`09` §3 decision 248); Jellyfin folders + scan with the API key named „felhom" read at use from Jellyfin's database (no key → one household step on the page) | `internal/mediawire` | G2: Media D wired into every app in one run; two more runs wrote nothing; G7: live catalog on 9201 | +| §7/§8 backup, export, removal | the override is read by no classifier, export or removal; „delete my data" never deletes a `:ro` or shared-library bind (R-1027); the safety list knows the userdata layout (R-1032) | `stacks/delete.go` | unit tests, each red-proved | +| §8 the wizard | a media app is not asked for a drive; its config goes on the default drive (R-1031) | `stacks/metadata.go`, `deploy.go` | unit test | +| §9 old installs | Sonarr/Radarr from before `bc54c01`: same-path root, `moveFiles:false`, rescan, old root removed, Radarr collections — only when `/media` is no longer bound | `mediawire` `repairOldRoot` | the K7 sequence as a unit test | +| §10 events | added / removed / returned recorded on the drive; the notice on the storage page and the dashboard with „Újraindítás most" (Plex/Jellyfin stream check first) and „Később" (03:30); per drive „restarted since"; containers holding a dead drive listed | `web/media_events.go`, `media_notice.html` | G2, G3: the notice within 6 s of a pull; one restart cured every app | +| §10 returned | the file browser is recreated at once (R-1038); a dead or removed drive is not bound into it | `web/handlers.go` | G3: `/srv/media_b` readable the second the drive was back | +| §10 safe removal | pause that drive's torrents → recreate the apps and the file browser without it → unmount → keep it registered; its return needs no household step; the torrents resume after the restart (R-1044) | `web/media_events.go`, `storage_handlers.go` | G4: only D's 2 torrents paused, every app let go before the unmount, back by itself 7 s after the disk, torrents seeding with no re-check | +| Plex | code asked after the start (`after_start` field) on the Plex page; the gate opens only when Plex says claimed, „Kész" refused before (R-1028, R-1045) | `web/media_events.go`, `setup_gate.go` | G5 on 9201: no code at install, gated, „Kész" 409; the claim itself: K6-2 by hand (the dashboard claim waits on a household code) | + +**Measured traps, kept here because each would be rebuilt:** the agent's disk `state` stays „attached" for a registered +disk that is gone — presence is the backing device; the periodic drive gate must leave a safely removed drive alone (it +raced the 32-second removal); qBittorrent 5 answers a good login with 204; Sonarr creates a qBittorrent category with no +save path when its client is saved before the wiring; **a Plex status probe (`/library/sections`, done 401) OPENED an +unclaimed Plex because Plex answers 401 for a moment while it starts — never gate on a status a starting app also +returns** (G7f). + diff --git a/documentation/architecture/capability-map-detail/D.md b/documentation/architecture/capability-map-detail/D.md index 294813b6..79b6225c 100644 --- a/documentation/architecture/capability-map-detail/D.md +++ b/documentation/architecture/capability-map-detail/D.md @@ -213,3 +213,27 @@ **Gap / roadmap (as written):** (`CAMPAIGN-3` F10/F11/F12 were the CRITICAL/HIGH *failures*; fixes shipped in agent v0.85 and were re-validated live in 4/6A — cite the validation, not the finding.) Residual: `skip-active` on `pct reboot` carried by the heal path; a NAS outage spanning a guest reboot can strand the share until agent restart (6A) --- + + +## D13 — Media apps see every drive; drive events handled per app + +*Map row: [D13](../00-capability-map.md#d13) · added 2026-10-15.* + +**Scenario:** Media apps see every drive; a drive added, pulled, back or safely removed is handled per app. + +**Components:** controller v0.313.0 (`stacks/media_library.go`, `internal/mediawire`, `web/media_events.go`), catalog +`0188430` (the `media_library` marker), hub v0.151.0 (the „drive came back" mail, R-1049). + +**Status:** **PROVEN-LIVE** on Tier-0 boxes (Tester 1, demo-hp 9201), 2026-10-15. Fleet delivery: golden 0.313.0 → the +operator's vouch → the floor (`09` §3 decision 247). + +**Evidence:** `audits/media-build-2026-10-15/G/` — G1 every stored path, library item and file identical after the move; +G2 a new drive wired into every app in one run, later runs write nothing; G3 a pulled drive loses nothing, the file browser +returns at once, one restart cures the apps; G4 a safe removal pauses only that drive's torrents, every app lets go before +the unmount, the drive returns by itself and the torrents resume; G6 13 hardlink pairs intact. Design and §11: +`architecture/15-media-library.md`. + +**Gap:** Plex's dashboard claim with a real code (the code is the household's); Prowlarr in the live catalog (R-1048). + +--- + diff --git a/documentation/backlog/CLOSED-ITEMS.md b/documentation/backlog/CLOSED-ITEMS.md index 6edc9016..58b272fc 100644 --- a/documentation/backlog/CLOSED-ITEMS.md +++ b/documentation/backlog/CLOSED-ITEMS.md @@ -26,6 +26,20 @@ --- +## 2026-10-15 — the media build (controller v0.313.0) + +| Row | What | Closed | Evidence | +|---|---|---|---| +| **R-1024** | **Links for the media apps: Sonarr and Radarr connected to the download apps and to a drive by the box.** (P3) | CLOSED 2026-10-15 — FIXED in controller v0.313.0 (`internal/mediawire`): every present drive wired into every media app after it comes up, after the media restart and on „Bekötés most”; idempotent (G2: two later runs wrote nothing; G7 on the live catalog). Reaches the fleet with golden 0.313.0 + the floor (R-1023) | `audits/media-build-2026-10-15/` | +| **R-1026** | **The dashboard's app „restart” restarts nothing.** (P3) | CLOSED 2026-10-15 — FIXED in controller v0.313.0: `RestartStack` = `up -d --force-recreate` with the stack's env, start times before/after, an error when nothing started (`TestRestartStack_R1026_*`, red-proved; G1–G3 every start time moved). Reaches the fleet with golden 0.313.0 + the floor (R-1023) | `audits/media-build-2026-10-15/` | +| **R-1027** | **„Delete my data too” deletes every bind under the app's drive — read-only and shared ones included.** (P2) | CLOSED 2026-10-15 — FIXED in controller v0.313.0: a removal never deletes a `:ro` or shared-library bind (`TestRemoveStack_R1027_*`, red-proved). Reaches the fleet with golden 0.313.0 + the floor (R-1023) | `audits/media-build-2026-10-15/` | +| **R-1031** | **The build must not ask a media app for one drive.** (P2) | CLOSED 2026-10-15 — FIXED in controller v0.313.0: a `media_library` app is not asked for a drive; its config goes on the default drive (`TestR1031_*`, red-proved). Reaches the fleet with golden 0.313.0 + the floor (R-1023) | `audits/media-build-2026-10-15/` | +| **R-1032** | **The delete safety list still names the old folder layout.** (P2) | CLOSED 2026-10-15 — FIXED in controller v0.313.0: `ProtectedHDDPaths` knows the userdata layout (`TestProtectedHDDPaths_R1032_*`, red-proved). Reaches the fleet with golden 0.313.0 + the floor (R-1023) | `audits/media-build-2026-10-15/` | +| **R-1037** | **Plex never notices new files: its automatic scan is off by default.** (P3) | CLOSED 2026-10-15 — FIXED in controller v0.313.0: the wiring switches Plex's „scan when a change is detected” on, with the server's own token read at use (decision 248); G1 wrote it on Tester 1. Reaches the fleet with golden 0.313.0 + the floor (R-1023) | `audits/media-build-2026-10-15/` | +| **R-1038** | **After a drive comes back, the file browser (and every app that binds it) still shows the DEAD drive: „I/O error”.** (P2) | CLOSED 2026-10-15 — FIXED in controller v0.313.0: a returned drive recreates the file browser at once (G3: readable the second it was back); a dead or removed drive is not bound into it; the dead mount is never turned into an empty folder (the media binds skip absent/dead drives). Reaches the fleet with golden 0.313.0 + the floor (R-1023) | `audits/media-build-2026-10-15/` | +| **R-1044** | **„Safe removal” of a media drive is not safe while the media apps run.** (P2) | CLOSED 2026-10-15 — FIXED in controller v0.313.0: a safe removal pauses only that drive's torrents, recreates the media apps and the file browser without it, unmounts, KEEPS it registered; its return needs no household step; torrents resume after the restart (G4). Reaches the fleet with golden 0.313.0 + the floor (R-1023) | `audits/media-build-2026-10-15/` | +| **R-1047** | **Installed Sonarr and Radarr from before app-catalog `bc54c01` lose their library path on their next start.** (P3) | CLOSED 2026-10-15 — FIXED in controller v0.313.0 (`mediawire` `repairOldRoot`): an install from before `bc54c01` is repaired once, no file moved, Radarr collections too (`TestRepairOldRoot_K7`, red-proved). Reaches the fleet with golden 0.313.0 + the floor (R-1023) | `audits/media-build-2026-10-15/` | + ## 2026-10-15 — operator rulings D1 and D2 (media build brief) | Row | What | Closed | Evidence | diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 82b9a046..eb563869 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -124,7 +124,7 @@ stopping line that lies. | **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC | | **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator | -## Apps & catalog — 18 rows (P2 1, P3 12, P4 5) +## Apps & catalog — 14 rows (P2 1, P3 8, P4 5) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| @@ -133,18 +133,14 @@ stopping line that lies. | **R-676** | Apps & catalog | P3 | **[P3-LOW] Watch: immich's first start restarted 12 times — decision 28's crash-loop stop (6 in 10 min) would stop it.** From the 2026-09-17 chaos night (DB connection dropped during the first-start geocoding import on a 6 GB guest; it did not recover that night). No healthy app in any drill evidence restarts on a first start (1831 samples, 40 live containers), so the threshold stands; this row exists so the first immich install under v0.269.x is watched. `audits/night-2026-09-24/A3/40-first-start-restarts.txt` **2026-09-25 night (read from source, v0.271.0): a DEPLOY's first start is NOT covered by decision 28's suppression** — `Deploying` clears when `compose up -d` returns (`deploy.go` "Clear deploying flag"), and `ObserveUnhealthy` then samples the app; an automatic update's step, verify and undo ARE covered (`Updating`, pinned by `TestD28_NoCrashLoopStopDuringAnAutomaticStep`). So a first start that restarts ≥ 6 times in 10 min is stopped — which R-676 already accepts for a broken first start; a healthy slow first start would be stopped too. **-- 2026-09-30: the first-start restarts are explained.** immich's first-start geodata import OOM-kills its database at 512M on a guest with no swap (R-732, measured: 61–104 kills); the 2026-09-17 chaos-night case (DB connection dropped during the import on a 6 GB guest) fits it. Fixed in the catalog (`56c4888`, 768M). The watch itself (decision 28 on a DEPLOY's first start) is unchanged. | **OPEN — P3; owner: CC (watch)** | — | — | CC | | **R-994** | Apps & catalog | P3 | **Seven apps peak above 80 % of a memory limit, and 23 apps' typical use is above their catalog `mem_request`.** MEASURED 2026-10-13 (`documentation/audits/sizing-2026-10-13/SIZING.md` §3, `evidence/catalog-check.json`). Near a limit — risk: the container is stopped for using too much memory: docmost (container 475 of 512 MB), romm (703 of 768), termix (117 of 128), uptime-kuma (227 of 256, tester-1 telemetry), grafana (435 of 512), immich (`immich-server` 1,665 of 2,048 during a 200-photo import), tandoor (827 of 1,024). No container was killed during the bench. Under-stated request (typical ÷ request): calcom ×4.0, adventurelog ×3.6, termix ×3.4, claper ×2.5, grafana ×2.2, komga ×2.2, docmost ×2.1, recipe-importer ×2.1, uptime-kuma ×2.1, zipline ×2.0, and 13 more ≤ ×1.8. Templates NOT changed (read-only run). | **READY** — filed 2026-10-13 | — | Raise the seven limits and the 23 requests in their templates, each through the catalog's harness; re-measure Immich with a larger library first | CC | | **R-1018** | Apps & catalog | P3 | **The OnlyOffice links (FileBrowser, Nextcloud) have no recorded live proof since the move to Proxmox.** `controller/internal/integrations/` holds two handlers; no audit after the Proxmox move shows a document opened from FileBrowser or Nextcloud in OnlyOffice (searched 2026-10-14). The media-app links of `15` §6 will copy this code, including its revoke-on-stop rule (`lifecycle.go:10-13`). | **OPEN** — filed 2026-10-14 | — | One live walk on a Tier-0 box: install OnlyOffice + FileBrowser, open a document, save, read it back; then stop one app and record what the link does | CC | -| **R-1026** | Apps & catalog | P3 | **The dashboard's app „restart” restarts nothing.** `RestartStack` runs `docker compose up -d`, which changes nothing when the compose did not change; it logged „Stack qbittorrent restarted successfully (took 0.1s)” while the container's StartedAt stayed the same (Tester 1, 2026-10-14, `audits/media-all-drives-2026-10-14/``D-plex-remove-deleted.txt`). So the button cannot make an app see a new drive or recover from a deleted bind source, and its success line is presence, not result. Stop + start did work. **Night 2026-10-14 (K5):** stop+start, or `up -d --force-recreate`, works in 2–10 s per app — but ONLY with the controller's environment: a bare `docker compose up -d --force-recreate` by hand lost every `${…}` value (Sonarr's and qBittorrent's routes became `Host(`.`)` → public 404, qBittorrent's password empty); the dashboard restart repaired it. FileBrowser has no `${…}` and recreated in 5.2 s. | **READY** — filed 2026-10-14 | — | Make restart a real restart through `composeExecCustomEnv` (`up -d --force-recreate` with the stack env), and make its log line state what changed; a test that asserts a new container start time. The restart notice of R-1023 depends on it | CC | -| **R-1024** | Apps & catalog | P3 | **Links for the media apps: Sonarr and Radarr connected to the download apps and to a drive by the box.** Measured calls and answer shapes: `audits/media-all-drives-2026-10-14/``tools/wire.py` + `C-wire-run2.txt`. Facts the code needs: the API key can be SET at install (`SONARR__AUTH__APIKEY`/`RADARR__AUTH__APIKEY`, measured 200/401); qBittorrent needs automatic torrent management on, or a category's folder is ignored; a Transmission entry must clear the default category („Cannot use Category and Directory”); Transmission's one incomplete folder must be off; a show with no tag gets no download app when every entry is tagged — SEEN LIVE 2026-10-14 18:50: the operator added two shows with a root folder each and no tag, and Sonarr queued 11 episodes as „Pending - Download client is unavailable” while all four entries passed Sonarr's own test (`audits/media-all-drives-2026-10-14/I-client-unavailable.txt`), so the drive choice must SET the tag; the category folders (`torrents/{tv,movies}`, `transmission/{tv,movies}`) must exist before the first grab or Sonarr raises a „remote path mapping” health error; the OnlyOffice revoke-on-stop rule (`lifecycle.go:10-13`) must NOT apply (it would delete the download entries at every restart). **Night 2026-10-14 (K2, `audits/media-drive-events-2026-10-14/``K2-6/K2-7`):** a NEW drive (Media C) made ready in every app by script in one run — qBittorrent categories, Sonarr/Radarr root folder + tag + two download entries, Plex and Jellyfin library folders + a scan, Plex „scan on change” on; the second run changed nothing (it re-sent equal qBittorrent category edits — the build checks before it writes). Two gaps stay: Plex needs a token (R-1046) and Jellyfin an API key (needs an admin session: the box has one only if it made the first admin). Narrowed 2026-10-14 night. | **NARROWED** — filed 2026-10-14; per-drive wiring proven idempotent on a third drive | R-1023 | Build with R-1023 from `15` §10 (the „added” rows) | CC | -| **R-1028** | Apps & catalog | P3 | **Plex's claim code can run out during the install, and the box cannot recover it.** plex.tv's code lives 4 minutes from the moment the household copies it; the clock covers the wizard, the image pull (`plexinc/pms-docker` is 351 MB, measured) and the first start. Plex reads `PLEX_CLAIM` only on its first run, and the field is `locked_after_deploy` — so a late code leaves an unclaimed server and the only way back is remove + install again. Reported by the operator during the 2026-10-14 test; on Tester 1's line the pull+start took 8.5 s and the claim worked (`audits/media-all-drives-2026-10-14/``G-auth/G3-plex-claim.txt`), so it bites on a slow line or a slow hand. **K6, 2026-10-14 night (`audits/media-drive-events-2026-10-14/``K6-1-unclaimed-plex.txt`):** an unclaimed Plex starts with no code at all, and `POST /myplex/claim?token=` on the RUNNING server reaches plex.tv (a wrong code → 500 after plex.tv's 401). So the box can pull + start first and ask for the code after: no race. A real claim needs the household's code — the morning step on the throwaway `plex-k6` on Tester 1. Narrowed 2026-10-14 night. **PROVEN 2026-10-15 morning (`K6-2-claim-after-start.txt`):** the operator's real code, given to the RUNNING unclaimed `plex-k6` → 200, `claimed` 0→1, `PlexOnlineToken` written, and the open door closed (no token → 401). `ADVERTISE_IP` was unset and the claim did not need it; whether clients find the server without it was not measurable (the throwaway had no published port). | **READY** — the claim-after-start path is proven; the build is owed | — | The build: install unclaimed with NO public route (R-1045), pull + start, then ask the household for the code and `POST /myplex/claim?token=` on the running server; read `claimed=1` back | CC | +| **R-1028** | Apps & catalog | P3 | **Plex's claim code can run out during the install, and the box cannot recover it.** plex.tv's code lives 4 minutes from the moment the household copies it; the clock covers the wizard, the image pull (`plexinc/pms-docker` is 351 MB, measured) and the first start. Plex reads `PLEX_CLAIM` only on its first run, and the field is `locked_after_deploy` — so a late code leaves an unclaimed server and the only way back is remove + install again. Reported by the operator during the 2026-10-14 test; on Tester 1's line the pull+start took 8.5 s and the claim worked (`audits/media-all-drives-2026-10-14/``G-auth/G3-plex-claim.txt`), so it bites on a slow line or a slow hand. **K6, 2026-10-14 night (`audits/media-drive-events-2026-10-14/``K6-1-unclaimed-plex.txt`):** an unclaimed Plex starts with no code at all, and `POST /myplex/claim?token=` on the RUNNING server reaches plex.tv (a wrong code → 500 after plex.tv's 401). So the box can pull + start first and ask for the code after: no race. A real claim needs the household's code — the morning step on the throwaway `plex-k6` on Tester 1. Narrowed 2026-10-14 night. **PROVEN 2026-10-15 morning (`K6-2-claim-after-start.txt`):** the operator's real code, given to the RUNNING unclaimed `plex-k6` → 200, `claimed` 0→1, `PlexOnlineToken` written, and the open door closed (no token → 401). `ADVERTISE_IP` was unset and the claim did not need it; whether clients find the server without it was not measurable (the throwaway had no published port). **BUILT 2026-10-15 (v0.313.0):** `after_start` field — not asked at install; the Plex page asks for the code once Plex runs and claims the running server; the gate opens only when Plex says claimed. Proven up to the code on 9201 (G5). | **NARROWED** — the dashboard claim with a real code is the last step | a household Plex code | Claim through the Plex page with a real code (G5c); then close | CC | | **R-1029** | Apps & catalog | P3 | **Plex's folder picker shows the drives as identical „media” folders.** With one bind per drive at the same path (`/mnt/felhom-drives//userdata/media`), the leaf of every library folder is `media`; the drive's name sits two levels up, so in Plex's picker the household sees several folders all called „media” (reported by the operator during the 2026-10-14 test on Tester 1). It conflicts with `15` §2 reason 2 (the same path everywhere). | **READY** — filed 2026-10-14 | the layout decision (STATUS D9) | Decide in `15` §2: the players (`:ro`, never linked) may get a readable path per drive (e.g. `/media/`) without breaking the hardlink rule, which binds only the download/library apps | CC | | **R-1036** | Apps & catalog | P3 | **The apps page lags the catalog by two apps.** `app-catalog-felhom.eu/templates/` holds **64** apps; `/alkalmazasok` and `/en/apps` render **63** cards, one of which (`filebrowser`) is built in and not a catalog template — so **62 catalog apps are listed**. Missing: **`plant-it`** and **`wger`**, both in the catalog since 2026-10-06/08. Their website assets are ALREADY present (`plant-it-logo.svg` + 3 screenshots, `wger-logo.png` + 3 screenshots, renamed to the catalog slug by `a3507761`, 2026-10-08) — only the cards were never written, so this is not a new-asset job. Found 2026-10-14 while reconciling the app count for the „felhő tárhely” page, where four places said **60** and the rest said **62**. **Done in that session:** every stated count on the site set to **62** (`index.html`, `szolgaltatasok.html`, `en/services.html` were 60), which is what the apps page actually shows — so no visitor can count the cards and find the number wrong. **62 is still not the catalog's number.** **To close:** write the two cards on both apps pages (gate 15 keeps the two pages' app sets equal, so they move together), then raise every stated count 62 → 64 in the same commit. **The operator chose this split at the 2026-10-14 STOP** over doing it inside the SEO change. | **OPEN** | — | write the `plant-it` + `wger` cards on both apps pages, then 62 → 64 everywhere, one commit | CC | -| **R-1037** | Apps & catalog | P3 | **Plex never notices new files: its automatic scan is off by default.** MEASURED 2026-10-14 on Tester 1 (`audits/media-all-drives-2026-10-14/K-plex-empty.txt`): the operator made the TV and Movies libraries with both drives' folders at ~18:35; 16 episodes and 2 films were imported from 19:07 on; at 19:15 Plex still showed 0 items, no scan running, and `Preferences.xml` carries neither `FSEventLibraryUpdatesEnabled` nor `ScheduledLibraryUpdatesEnabled` (Plex's defaults: both off). One manual `/library/sections//refresh` → 3 shows and 2 films. A household sees an empty Plex and concludes the box is broken. **Night 2026-10-14 (K2):** the box can switch it on by script — `PUT /:/prefs FSEventLibraryUpdatesEnabled=1` (+ `FSEventLibraryPartialScanEnabled=1`), read back from `Preferences.xml`. Narrowed 2026-10-14 night. | **NARROWED** — the call is measured; needs the Plex token of R-1046 | R-1046 | Seed it at install with R-1024's wiring | CC | | **R-76** | Apps & catalog | P4 | **FileBrowser-created folders break the setgid chain, and a drop-zone's mode is not stable** **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-26, size S, roadmap state `idea (surfaced by the R-75 spike, 2026-07-26)`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** **2026-10-06 night: not a catalog fix** — the setgid chain is set by the controller's FileBrowser setup; a controller row. | — | Two related findings from `audits/SPIKE-catalog-data-paths-2026-07-26.md` P3/P5, both **pre-existing** and deliberately left alone by that spike. **(a)** FileBrowser Quantum 1.3.3 creates files `0644` and folders `0755` and does **not** propagate the setgid bit — even though the entrypoint wrapper's `umask 002` really is in effect (`/proc/1/status` `Umask: 0002`). Group inheritance itself works (a file uploaded into a 2775 group-100 dir landed group 100, not the process gid 1000), so the convention's *group* half holds and only its *mode* half is lost. The consequence is proven with a control: inside a UI-created `0755` folder a gid-1000 process's file landed group **1000**, while the identical write into the 2775 parent landed group **100**. So **any folder a customer creates through FileBrowser breaks the shared-group chain one level down.** Latent today — every userdata-touching catalog app that declares an identity declares uid/gid **1000**, the same uid FileBrowser runs as, so owner permissions mask it; it bites the day a content app runs as a different non-root uid with gid 1000. The comment at `infra/infra.go:156` is right that the image ignores `-e UMASK` but does not say t | CC | | **R-707** | Apps & catalog | P4 | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). **2026-09-29 (controller v0.280.0, catalog `d0e7e2e`):** every class-3 app fixed — mealie, wger, calibre-web by `after_install` (calibre-web with the new `password:24:special`), proven on 9202 fresh installs (`audits/login-gate-2026-09-29/D/`); the setup gate (decision 46, spike PASSED) built and live on immich, n8n, audiobookshelf (probes measured) and uptime-kuma (button) (`…/C/`); romm's and zipline's stale notes removed. **Left: 30 class-4 apps** — gate each (probe measured on 9202 where one exists — 11 upstream candidates listed in `…/B/B-VERDICT.md` §3; the button otherwise). **2026-09-29 afternoon (controller v0.281.0, catalog `6faf432`):** 28 more class-4 apps gated — 32 of 34 — each proven on 9202 (`audits/gate-rollout-2026-09-29/`B): stranger → gate page / 401, household reached the first-setup screen, the gate opened (9 by a measured probe, the rest by the press), the app answered after. seerr, outline, rallly: gated, their opening needs a media server / e-mail (not proven). **Left:** wanderer (R-714); plant-it is not installable. | **NARROWED** (2026-10-03 triage: the row's verdict was finished, but it names open work no other row carries — seerr, outline and rallly are gated, but the gate OPENING is not proven (needs a media server / e-mail)) — **CLOSED — 2026-09-29 (the rest → R-714)** | — | — | CC | | **R-905** | Apps & catalog | P4 | **wger's collected style files (283 MB) go into every wger backup, though the app rebuilds them at every start.** Since R-762's fix (`DJANGO_DEBUG=False` + `collectstatic`, catalog `cf1ed43`) the static files sit in a named volume that the backup legs copy like data (measured 283 MB, `audits/design-build-2026-10-06/`); `collectstatic` regenerates them, so they are rebuildable bytes in every Tier-1 unit, Tier-2 copy and off-site snapshot. wger is `lifecycle: hidden` and no box runs it (hub read 2026-10-08). Options to weigh: a non-backed-up volume class for regenerable data, or an anonymous volume. Found by the R-762 helpers 2026-10-08. | **OPEN — needs a design before wger is shown** | — | Design the „regenerable volume" exclusion (catalog + controller backup legs) | CC | | **R-995** | Apps & catalog | P4 | **Removing an app through the product can leave unnamed Docker volumes and an empty import folder behind.** MEASURED 2026-10-13 on demo-felhom after 61 removals (`documentation/audits/sizing-2026-10-13/METHOD.md` „Teardown”, `evidence/leftover-volumes-after-bench.txt`): 8 unnamed volumes (4–72 KB; immich 1, romm 2, privatebin 3, onlyoffice 2 — volumes the images declare themselves) and `userdata/import/{paperless,calibre,grimmory}` (empty, created by the deploy). The removal answered 200 and its own leftover check was clean, because it looks only at labelled volumes. No data risk; disk and tidiness only. Removed by name on the bench. | **READY** — filed 2026-10-13 | — | Make the removal account for the containers' unnamed volumes (and an empty import folder it created), with a test that deploys an image that declares a VOLUME | CC | | **R-1043** | Apps & catalog | P4 | **Sonarr, Radarr and Prowlarr build their login redirect as an absolute `http://` URL.** They trust forwarded headers only from their „Trusted networks” (the household set `192.168.0.0/24`; traefik is `172.18.x`), so they ignore `X-Forwarded-Proto: https` and answer `Location: http:///login` to an https visitor. Since controller v0.312.0 that hop is sent back to https by traefik (a GET, no password), so it is cosmetic; a template could seed traefik's network as trusted. (`audits/https-only-2026-10-14/``1A-3-4.txt`) | **OPEN** — filed 2026-10-14 | — | Seed the docker network as a trusted proxy in the *arr templates (with the R-1030 seed), measure the redirect | CC | -| **R-1047** | Apps & catalog | P3 | **Installed Sonarr and Radarr from before app-catalog `bc54c01` lose their library path on their next start.** MEASURED 2026-10-14 night on the bench (`audits/media-drive-events-2026-10-14/``K7-old-install.txt`): the old template mounted the library at `/media/tv` / `/media/movies`; after the new template's one same-path bind the root folder is missing (health „Missing root folder”), the records stay, nothing is deleted. Repair through the apps' own API, no file moved (same inode): add the same-path root folder, `PUT /series/editor` (`/movie/editor`) `rootFolderPath` + `moveFiles:false`, rescan, delete the old root folder. Left over: Radarr's collections keep the old root folder (a separate setting), and the health page shows the old warning until its next check. | **READY** — filed 2026-10-14 night | — | A one-time migration step in the controller for Sonarr/Radarr installed before `bc54c01` (the K7 calls + Radarr's collection root); find which boxes have one | CC | | **R-1048** | Apps & catalog | P3 | **Prowlarr into the live catalog (full new-app checklist), linked by the box.** Today it is in the drill catalog only (test, 2026-10-14): config in a named volume, no media folders; the box linked it to Sonarr and Radarr by script (Prowlarr → Settings → Apps, full sync — `audits/media-all-drives-2026-10-14/tools/wire.py`). The live catalog needs the full new-app checklist (`app-catalog-felhom.eu/REUSE.md`), Hungarian copy, the apps page, and the link made by the box at install. | **READY** — filed 2026-10-14 night | — | Add it with the checklist; the link with R-1024 | CC | @@ -192,19 +188,14 @@ stopping line that lies. | **R-878** | Backup & restore | P4 | **A catch-up (R-871) runs the database-dump leg in the DAY, and that leg stops an app with a volume for its copy — the household may notice the stop, and a large volume makes it longer.** MEASURED 2026-10-05 on demo-felhom: the catch-up at 08:25:02 stopped opengist, copied 182.5 KB, started it again — about 1 s, then a few seconds of `health: starting`; the night does exactly the same, unseen. Nothing measured for a large volume. Fix direction (if it matters): skip the volume copy of a running app in a DAYTIME catch-up and leave it to the next night, or warn. `audits/catchup-2026-10-05/partA/live-demo-felhom.txt` | **READY — owner: CC** **2026-10-05 (burn-down night): NEEDS A LIVE MEASUREMENT** — a timed catch-up on 9202 for an app with a multi-GB volume, then the skip-or-warn choice. | — | measure a large volume first | CC | | **R-32** | Backup & restore | P4 | **[P2-HIGH] RESET must purge the customer base dir; the orphan card must stay honest; unattributed bytes must be visible.** The rehearsal's S7 said in advance that an orphan card would BE a finding — and one appeared (16:58:14). Cause: RESET's `"hetzner":"ok"` leg destroys the sub-account, but **a Hetzner sub-account is an access-control object, not a data object** — its directory survives, so re-enabling offsite recreated an account over the previous lifecycle's ciphertext, encrypted under a key that same RESET had destroyed. **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-21, size M, roadmap state `idea`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** **2026-10-06 night: design written** (`audits/night-burndown-2026-10-06/design-R-32.md`) — pick A: purge the repo + set-aside copies through the sub-account's own password login BEFORE deleting it (no main-account credential); the „repo data destroyed" comment at `hub/internal/offsite/offsite.go:276-280` is false for the shared tier; part (3) waits on a read-only measurement (`du` on port 23). Operator: agree to route A. **2026-10-07 07:58: `09` §3 decision 167 — option A yes, and delete the old RESET leftovers once.** **2026-10-07 (Part D step 2, read only):** the hub shows bx11 holding 3.3 GB of data while the four live customers account for ~0.6 GB → **~2.7 GB belongs to no live customer**; no live customer shows a set-aside folder, so the leftovers are in homes whose sub-account is gone, reachable only by the pool box's MAIN account. **The one-time clean-up is STOPPED:** no main-account login for bx11 is held on DooPlex (the API token in the credentials file sees no box). Needs: the main-account login, or the operator lists and deletes in the Hetzner console (`audits/day-2026-10-07/D/D2-listing-readonly.txt`). **2026-10-07: option A DELIVERED (hub 0.142.0):** RESET purges the repo and every `.orphaned-*` through the sub-account's own login before deleting it, and keeps the sub-account if the purge fails (red-proved, `audits/day-2026-10-07/D/`). Not yet seen live (no scratch RESET today). **The one-time clean-up of the ~2.7 GB of leftovers is STOPPED:** it needs the pool box's main-account login (see above). **2026-10-07 14:43 — `09` §3 decision 173: leave the ~2.7 GB of old leftovers for now; re-ranked P2 → P4.** Option A itself is delivered (hub 0.142.0). What stays open is ONLY the leftovers: they were computed (box total minus live customers), never listed, and deleting them needs the pool box's MAIN login, which reaches every customer's data. **Blocker: a safe main-account path.** | — | **Ruling from the run (three parts, deliberately separate):** (1) because RESET destroys custody, the ciphertext it leaves behind is unrecoverable **BY DESIGN** → RESET gains a **main-account purge of the customer base dir** (the existing operator ack already covers it); (2) the **move-aside guard STAYS** for reinstall-*without*-RESET — there custody survives and the card's "history recoverable" promise is true (R-26 depends on exactly that); (3) the operator **Restic tab shows per-customer directory bytes vs attributed snapshot bytes**, so dead data cannot hide. Measured on the pool box that night: **49 M attributed** (2 snapshots, 48.717 MiB) against **1.4 G + 3.0 M unattributed** across TWO `.orphaned-*` dirs. Evidence `restic-and-pool.txt` | CC | -## Storage & devices — 9 rows (P2 6, P3 2, P4 1) +## Storage & devices — 4 rows (P2 1, P3 2, P4 1) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| -| **R-1027** | Storage & devices | P2 | **„Delete my data too” deletes every bind under the app's drive — read-only and shared ones included.** MEASURED 2026-10-14 on Tester 1: removing a throwaway Plex (drill template: `/mnt/felhom-drives/media_a/userdata/media` bound `:ro`, `HDD_PATH=/mnt/felhom-drives/media_a`) with `remove_hdd_data:true` logged „Removed HDD data: /mnt/felhom-drives/media_a/userdata/media (80K)” — the WHOLE media folder of the drive, which four other apps were using. `ParseComposeHDDMounts` counts a literal path under `HDD_PATH` as the app's own data, and `ProtectedHDDPaths` does not protect `userdata/media` (R-1032). Only an empty test skeleton was lost; it was rebuilt by the controller's next file-browser sync and the apps restarted (`audits/media-all-drives-2026-10-14/``D-plex-remove-deleted.txt`). Today's LIVE templates bind `${USERDATA_PATH}` and are not hit (R-800); **the media-library build (R-1023) would be, unless removal learns that a shared library is never one app's data.** The note „literal binds are never removed” in this session's first draft was WRONG and is retracted. | **READY** — filed 2026-10-14 | — | Removal: never delete a `:ro` bind nor anything under `userdata/`; a test that removes a reader app and asserts the shared folder survives (red-proof first); `15` §8 | CC | -| **R-1023** | Storage & devices | P2 | **Build: media apps see every drive, and the box says when they must restart (operator rulings 2026-10-14, `09` §3).** Scope from `architecture/15-media-library.md`: one bind per drive of `userdata/media/` at the same path; the drive-absent gate per bind (today it keys on the app's one `HDD_PATH`, and the boot gate waits only for THAT drive — measured after the 17:25 power cut); the restart notice after a drive is added, removed or comes back, with „restart now” / „later” and a per-drive „apps restarted since” mark; removal, export and backup classes taught that a shared library is no app's own (R-1027, R-1019). **S5/S6 measured 2026-10-14:** a pulled drive stops none of the media apps (they key on another drive), they keep a dead view; on return nothing restarts them — so the restart notice MUST also cover „a drive came back”, the restart must be stop+start, and Sonarr/Radarr's own health page says nothing about an unreachable root folder, so the box must say it. **Night 2026-10-14 measured every event × app (`15` §10, `audits/media-drive-events-2026-10-14/`):** nothing is lost while a drive is gone (K1), so the build needs NO scan/unmonitor/torrent guard; it DOES need: (1) one bind per PRESENT drive only — a bind to an absent path makes the app unstartable (K3-4: 500 „error while creating mount source path … permission denied”); (2) the media apps recreated without the drive BEFORE a safe removal unmounts it (R-1044); (3) a returned drive restarts every container that binds it, FileBrowser included (K5: 2–10 s per app); (4) „restart now” cuts a playing stream (K5-4: cut, back in 22 s) — check Plex/Jellyfin sessions first or offer „later”; (5) the stale view is found with NO disk I/O: each container's `/proc//mountinfo` shows the old device with `shutdown` (K4). Narrowed 2026-10-14 night. | **NARROWED** — filed 2026-10-14; every drive event measured (`15` §10); design complete for a build | — (decision 245, option A) | A build task from `15` §10 and the build plan in `REPORT-night-2026-10-14.md` | CC | -| **R-1031** | Storage & devices | P2 | **The build must not ask a media app for one drive.** Today every media template carries a required `HDD_PATH` field („Médiatár útvonal”, `locked_after_deploy`), so the wizard makes the household pick ONE drive — the opposite of `09` §3 decision 237 — and the drive gate, removal and backup key on that one drive (`15` §4, §8). The test rig kept the field only because the deploy needs it. | **READY** — filed 2026-10-14 | R-1023 | In the build: a media app (`15` §3 marker) has no drive field; its own config stays on the system SSD; its library binds come from the enrolled drives | CC | -| **R-1032** | Storage & devices | P2 | **The delete safety list still names the old folder layout.** `ProtectedHDDPaths` (`felhom-controller/controller/internal/stacks/delete.go:240`) protects `/media` and `/Dokumentumok` — the layout before `userdata/` — and NOT `/userdata`, `userdata/media` or `userdata/documents`. That is why removing a Plex deleted `userdata/media` (R-1027). It touches every app with a drive bind, not only media apps. | **READY** — filed 2026-10-14 | — | Protect the current skeleton roots (`userdata` and its top folders) and keep the old names; a test that fails when a skeleton root is missing from the list (red-proof first). Fix with R-1027 | CC | -| **R-1038** | Storage & devices | P2 | **After a drive comes back, the file browser (and every app that binds it) still shows the DEAD drive: „I/O error”.** MEASURED 2026-10-14 on Tester 1 (`audits/media-all-drives-2026-10-14/S5-S6/`): Media B's disk was pulled at 20:10:23 and re-attached at 20:41:00. The guest kept a dead ext4 mount of `/mnt/felhom-drives/media_b` the whole time (`mountpoint` = yes, `ls` = Input/output error); on return the agent bound the new disk ON TOP of it (two stacked mounts), the controller logged „drive RETURNED … restarted gate-stopped apps” and „FileBrowser sync — no config/compose change, ensured running without recreate” — so FileBrowser kept the dead mount: `ls /srv/media_b/media/tv` → I/O error, 4 minutes after the drive was back. The same was true for all six media apps until they were stopped and started. During the pull the box also tried to rebuild the skeleton ON the dead mount („chmod … input/output error”), because the drive-absent gate asks „is it a mountpoint?” and a dead mount answers yes. Today's product: any household whose USB drive drops and returns hits this in FileBrowser. **And the household cannot fix it:** FileBrowser is a protected stack — the dashboard answers `POST /api/stacks/filebrowser/stop` and `/start` with 403 „cannot stop protected stack filebrowser” (measured 20:46) — so only a restart of the whole box clears it. **Night 2026-10-14 (K1, K4, `audits/media-drive-events-2026-10-14/`):** the DEAD mount is what keeps a household's libraries safe — every app treats its I/O errors as „unavailable” and deletes nothing (K1); an EMPTY folder in its place could read as „all files deleted”. So the fix must NOT replace the dead mount by an empty folder under a running app. The no-I/O check is `/proc/self/mountinfo` super options containing `shutdown` (K4 check A); a timed `stat` said „ok” on the dead drive from the cache (check B — unreliable, do not use). Narrowed 2026-10-14 night. | **NARROWED** — the detection is measured; the safe direction is fixed | — | With R-1023: on return restart every container that binds the drive; gate and skeleton read `shutdown` from mountinfo; never an empty folder under a running media app | CC | +| **R-1023** | Storage & devices | P2 | **Build: media apps see every drive, and the box says when they must restart (operator rulings 2026-10-14, `09` §3).** Scope from `architecture/15-media-library.md`: one bind per drive of `userdata/media/` at the same path; the drive-absent gate per bind (today it keys on the app's one `HDD_PATH`, and the boot gate waits only for THAT drive — measured after the 17:25 power cut); the restart notice after a drive is added, removed or comes back, with „restart now” / „later” and a per-drive „apps restarted since” mark; removal, export and backup classes taught that a shared library is no app's own (R-1027, R-1019). **S5/S6 measured 2026-10-14:** a pulled drive stops none of the media apps (they key on another drive), they keep a dead view; on return nothing restarts them — so the restart notice MUST also cover „a drive came back”, the restart must be stop+start, and Sonarr/Radarr's own health page says nothing about an unreachable root folder, so the box must say it. **Night 2026-10-14 measured every event × app (`15` §10, `audits/media-drive-events-2026-10-14/`):** nothing is lost while a drive is gone (K1), so the build needs NO scan/unmonitor/torrent guard; it DOES need: (1) one bind per PRESENT drive only — a bind to an absent path makes the app unstartable (K3-4: 500 „error while creating mount source path … permission denied”); (2) the media apps recreated without the drive BEFORE a safe removal unmounts it (R-1044); (3) a returned drive restarts every container that binds it, FileBrowser included (K5: 2–10 s per app); (4) „restart now” cuts a playing stream (K5-4: cut, back in 22 s) — check Plex/Jellyfin sessions first or offer „later”; (5) the stale view is found with NO disk I/O: each container's `/proc//mountinfo` shows the old device with `shutdown` (K4). Narrowed 2026-10-14 night. **BUILT 2026-10-15 — controller v0.313.0 + catalog `0188430` (`15` §11; `audits/media-build-2026-10-15/`): proven on Tester 1 (G1–G4, G6) and demo-hp 9201 (G7).** What is left is delivery. | **WAITING-ON-OPERATOR** — golden 0.313.0 baked; the vouch and the floor are the operator's (decision 247) | the operator's vouch of golden 0.313.0, then the floor | After the floor: read back that demo-felhom and Tester 1 report 0.313.0 through the normal path; then close | CC | | **R-330** | Storage & devices | P3 | **Disk health Phase 2 — the three SMART attributes the wire does not carry.** The failing drive's most telling counter was **187 `Reported_Uncorrect`**, sitting at normalized **1** against threshold **0** with a raw count of **1001** — one point from failing and structurally unable to get there. Also wanted: **199 `UDMA_CRC_Error_Count`** (cabling) and **188 `Command_Timeout`**. None are on the agent→controller wire today, so v0.215.0's ladder could not use them. Phase 2 also persists periodic SMART **samples** (the right home is `metrics.MetricsStore`, NOT the Phase-1 state file, which is one record per disk and must stay that way). **This is a declared WIRE change, so under the G-1 gate the hub must model the new fields in the SAME session** — that is precisely why it was kept out of Phase 1, where it would have turned a one-word severity fix into a three-repo change | **READY** — **2026-10-12: the wire half is CLOSED in this row** — released in agent v0.150.0 (boxes run 0.154.0), controller and hub decode 187/188/199. **Only open work:** the verdict slice (use the three counters in the disk verdict and the household's words) and persisting samples. **READY (M) — NEW 2026-08-14** **2026-10-06 night: the wire half BUILT on main (unreleased):** the agent sends SMART 187 `reported_uncorrect`, 188 `command_timeout` (raw, vendor-packed on some drives — carried as reported), 199 `udma_crc_errors` (pointer + omitempty: unknown is absent, never 0); the controller decodes them and the hub's host-report mirror models them (G-1 wire gate green; it convicted the agent half alone). **Carried only** — no verdict, banner, mail or alarm reads them (`TestR330_CountersChangeNoVerdictYet`); using them changes what a household is told and is the next slice, with persisting samples. Red-proofs `audits/night-burndown-2026-10-06/r330/`. Ships with agent v0.150.0 and the next controller and hub releases. | R-328 (closed) | The verdict slice: 187/188/199 into the disk verdict with tests; then persist samples | CC | | **R-332** | Storage & devices | P3 | **The new Hiba-from-counters path has never fired on real hardware.** v0.215.0's whole point is a verdict the product could not previously reach, and it is proven only against the committed fixture's values in unit tests (12 scenario groups, 11 of 12 red-proofs failing as required). The live validation on demo-hp proved the **negative** — three healthy disks still read Rendben across the deploy, no false alert — and the **severity wire** end to end, but no live disk has actually reached Hiba. **This is the honest gap and it must not be closed by pointing at the fixture tests**: the drive that produced the fixture is in DooPlex, which is Tier 2 and never a drill target, and the demo boxes are all-flash and healthy | **WATCHING — NEW 2026-08-14, NARROWED same day.** One item originally in this gap is now PROVEN LIVE: the **persisted state surviving a controller restart**. The v0.215.0→v0.216.0 redeploy destroyed and rebuilt the container, and the new one read back a `changed_at` written by the PREVIOUS version (`2026-08-14T07:23:14.640216851Z`, still intact at 09:31:35Z) instead of re-baselining — Scenario L on real hardware, not just the production-path unit test. **What remains unproven is the verdict itself, plus the stronger restart half: an already-ALERTED disk not re-alerting** | a real degrading disk, or an injection harness | **Closing condition:** a live disk reaching Hiba from counters, OR a deliberate injection through the REAL pipeline (agent `/disks` → controller check → hub event), not a hand-set verdict | CC | | **R-331** | Storage & devices | P4 | **Disk health Phase 3 — growth-rate detection, and retiring the static 64.** The v0.215.0 count backstop (64 unreadable sectors → Hiba) is **a judgement from ONE drive**: the observed benign excursion peaked at 16 and cleared inside an hour, and the terminal run passed 64 at 13 Aug 11:28 and never came back. It is deliberately a backstop BEHIND the sustain rule, not the primary signal, but it is still a magic number tuned on a single sample and it will be wrong for some drive. With Phase 2's history the box can ask the question that actually matters — *is this count climbing, and how fast* — which distinguishes a drive with eight stable aging sectors from one adding forty a day, something no static threshold can do. Revisit 64 when that exists | **READY (M) — NEW 2026-08-14** | R-330 | Growth-rate rule over persisted samples; re-derive or delete the static 64 | CC | -| **R-1044** | Storage & devices | P2 | **„Safe removal” of a media drive is not safe while the media apps run.** MEASURED 2026-10-14 night on Tester 1 (`audits/media-drive-events-2026-10-14/``K3-3-eject.txt`): `POST /api/storage/eject` for Media C made the agent detach the guest's bind (the guest then shows an EMPTY folder) and keep the raw host mount, and the controller deregistered the drive — but the running media containers still HELD the filesystem and read its files after the „safe” removal. So pulling the disk then is a pull of a mounted, in-use filesystem. Also: after re-attaching, the agent mounted it again but the drive stayed unregistered until „Meglévő meghajtó csatolása” (`register-mounted`) — a household step after every safe removal. Restarting an app after the eject failed (K3-4: the bind source is gone → 500). | **OPEN** — filed 2026-10-14 night | R-1023 | Safe removal = pause that drive's torrents → recreate the media apps without its bind → unmount; keep the drive registered as „removed safely” so its return needs no household step; a test on the order of the three | CC | ## Security & access — 21 rows (P1 2, P2 4, P3 9, P4 6) @@ -229,7 +220,7 @@ stopping line that lies. | **R-779** | Security & access | P4 | **[P3-LOW] Part A's "two outside addresses seen as two" is proven through the simulated tunnel only; on the REAL tunnel the second outside address (ep0, one request allowed) was refused by Cloudflare's edge with 403 and never reached the box.** Measured 2026-10-01 19:51 UTC (`audits/visitors-2026-10-01/A/L2-demo-hp-real-tunnel.txt`): no log line on demo-hp; demo-hp's box has no geo restriction in its settings, so a Cloudflare ZONE rule (country or bot, not read) refused a German datacenter address. DooPlex's own address on the real tunnel was seen as itself. **Needs:** one sign-in from a second Hungarian address (the operator's phone off wifi) while DooPlex is locked out — 2 minutes; and say which Cloudflare rule refused ep0. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (a phone), CC reads the logs** **Re-ranked 2026-10-03: P3→P4: a proof gap on the real tunnel; operator-only follow-up.** | — | — | CC + operator | | **R-904** | Security & access | P4 | **Cloudflare can read every household's app traffic; replacing it with our own relay is a later item.** Facts (reviewer discussion 2026-10-08; `01`): app traffic and the dashboard reach the box through the Cloudflare Tunnel (`01` §5 trust table, rows end-user ↔ apps and customer ↔ controller UI; §7), and the tunnel's public end is Cloudflare's edge, where TLS ends — so Cloudflare can technically read that traffic (the FAQ says so since 2026-10-08, R-900; „TLS ends at the edge" is not written in `01` — add it there). What Cloudflare gives today, free: inbound reach with no router setup, the CGNAT answer (`01` §4, §7); certificates (`01` §7, the free tier covers one level below a zone); the geo-WAF the hub enforces (`01` §5 last row, §7); flood protection (not in `01`). The alternative named: our own EU relay over WireGuard with TLS passthrough by SNI, certificates on the box, the geo-block on the relay. Its costs: one more machine the operator keeps up, and a single point of reach for every box; weaker flood protection; about a week of work after a spike. Operator ruling 2026-10-08 09:07 (`09` §3 decision 184): a later item. | **DEFERRED** — **2026-10-10: the asked sentence is in `01` §7** („TLS ends at Cloudflare's edge …”); the spike stays deferred. **DEFERRED — after the first customers (operator ruling 2026-10-08 09:07)** | — | A spike after the first customers (the relay's reach, cost and flood behaviour, measured) | operator | | **R-1040** | Security & access | P4 | **Until the household makes Sonarr's or Radarr's first admin, any container on the box can read the app's API key.** Measured on the bench 2026-10-14 with the R-1030 seed in place: `AuthenticationRequired=Enabled`, `AuthenticationMethod=None` — `/initialize.json` from another container answers 200 with `apiKey`. Outsiders are blocked by the box's setup gate in that window; a compromised other app is not. | **OPEN** — filed 2026-10-14 | — | Seed the API key at install (`SONARR__AUTH__APIKEY`, measured to work) so the box knows it, and close the window by telling the household in the first steps; or seed a Forms user | CC | -| **R-1045** | Security & access | P2 | **An unclaimed Plex answers everything without a token to a private address — and through the tunnel every request comes from one.** MEASURED 2026-10-14 night on a throwaway `plex-k6` on Tester 1 (`audits/media-drive-events-2026-10-14/``K6-1-unclaimed-plex.txt`): with no claim, `/library/sections`, `/:/prefs` and `/myplex/account` answer 200 with NO token from the Docker network. Through cloudflared, Plex sees cloudflared's private address, so a publicly routed unclaimed Plex would be open to anyone — server settings and every library. R-1028's late code leaves exactly such a server. NOT measured over the public path (the throwaway had no route). Harm if true: a household's media and Plex settings open to the internet. **NARROWED 2026-10-15 (Part A, `audits/media-build-2026-10-15/A-plex-gate/`):** the live catalog's Plex has `setup_gate: true` (app-catalog `9a23ee5`). Proven on demo-hp 9201 with an EXPIRED claim code: the server came up unclaimed and open on the docker network (`/library/sections`, `/:/prefs` 200 with no token), while a stranger over the public path got only the gate (HTML → 302 to the dashboard's `/__gate/start`; `/identity`, `/library/sections`, `/:/prefs` → 401 „waiting for its first setup”), the same on the LAN path; the household passed with its dashboard session and reached Plex. Removing the app removed the gate file 9 s later (the tick). **Left open:** the gate opens on the household's „Kész, beállítottam” even while Plex is still unclaimed (no probe: Plex answers XML to the probe's request) — closed by R-1028's build (the controller claims and opens on a real claim). | **NARROWED** — the public path is closed by the gate; the „Kész” button on an unclaimed server is the rest | R-1028 | With R-1028: the gate opens only when Plex reports `claimed` (the controller asks with `Accept: application/json`) | CC | +| **R-1045** | Security & access | P2 | **An unclaimed Plex answers everything without a token to a private address — and through the tunnel every request comes from one.** MEASURED 2026-10-14 night on a throwaway `plex-k6` on Tester 1 (`audits/media-drive-events-2026-10-14/``K6-1-unclaimed-plex.txt`): with no claim, `/library/sections`, `/:/prefs` and `/myplex/account` answer 200 with NO token from the Docker network. Through cloudflared, Plex sees cloudflared's private address, so a publicly routed unclaimed Plex would be open to anyone — server settings and every library. R-1028's late code leaves exactly such a server. NOT measured over the public path (the throwaway had no route). Harm if true: a household's media and Plex settings open to the internet. **NARROWED 2026-10-15 (Part A, `audits/media-build-2026-10-15/A-plex-gate/`):** the live catalog's Plex has `setup_gate: true` (app-catalog `9a23ee5`). Proven on demo-hp 9201 with an EXPIRED claim code: the server came up unclaimed and open on the docker network (`/library/sections`, `/:/prefs` 200 with no token), while a stranger over the public path got only the gate (HTML → 302 to the dashboard's `/__gate/start`; `/identity`, `/library/sections`, `/:/prefs` → 401 „waiting for its first setup”), the same on the LAN path; the household passed with its dashboard session and reached Plex. Removing the app removed the gate file 9 s later (the tick). **Left open:** the gate opens on the household's „Kész, beállítottam” even while Plex is still unclaimed (no probe: Plex answers XML to the probe's request) — closed by R-1028's build (the controller claims and opens on a real claim). **2026-10-15 (v0.313.0):** the controller asks Plex itself whether it is claimed — after the claim on the Plex page and before „Kész” may open the gate (`TestPlexDonePress_RefusedWhileUnclaimed`; G5 on 9201: no code, gated, „Kész” 409). Two background security reviews of the catalog pushes found the old-controller half: an optional code (fixed: required again, `after_start` for 0.313.0) and the remaining residual — on a controller before 0.313.0 a code that ran out leaves an unclaimed Plex that „Kész” can open. A status probe meant to close that OPENED an unclaimed Plex while Plex started (G7f; removed within the hour). The residual ends when the floor carries 0.313.0. | **WAITING-ON-OPERATOR** — closed by the floor at 0.313.0 | the floor at 0.313.0 | After the floor: close | CC | | **R-1050** | Security & access | P4 | **Raise HSTS to one year on or after 2026-11-15, if no https trouble was reported.** `09` §3 decision 249 (D2 = A, operator 2026-10-15): HSTS stays `max-age=86400` now; after a month with no https trouble it becomes one year (`31536000`); never preload, never `includeSubDomains`. The value is `HSTSMaxAgeSeconds` in `felhom-controller/controller/internal/infra/infra.go`, pinned by `TestRenderForwardedHeaders_HTTPSOnly`. „Trouble” = any report of a name that will not open, or a box answering http after the floor. | **WATCHING** — filed 2026-10-15 | the date | On or after 2026-11-15: read the register and the operator's mail for https trouble since 2026-10-15; none → a controller release with one year (test updated), bake → vouch → floor | CC | ## Box system & updates — 11 rows (P2 1, P3 7, P4 3) @@ -262,7 +253,7 @@ stopping line that lies. | **R-724** | Monitoring & notifications | P3 | **[P3-LOW] The status pages disagree with each other in small ways a household notices.** MEASURED 2026-09-29 on a fresh box: Beállítások reads „Mentés ütemezés 02:30 / 03:00" while Biztonsági mentés says 02:30 / 03:30 / 04:15 / 04:30–08:30; Beállítások shows a raw `2026-09-29T19:22:30Z`; its „Helyi cím (LAN)" and „Átjáró" read „nem állapítható meg" on the page the household is asked to read out for remote help; the backups overview still says „Következő mentés — 0 órája" (the age of the LAST run, under the word "next" — noted 2026-09-14, never filed); the dashboard shows the backup at 19:33 where the backup pages say 21:33 (R-500, still reproducing). **FIXED 2026-09-30 (controller v0.283.0):** Beállítások shows the real schedule and a local time; the dashboard's last backup is local time (R-500); „az előző N órája készült" replaces „0 órája". **NARROWED — remaining:** „Helyi cím (LAN)" / „Átjáró" read through the file-sharing container, so a box without it says „nem állapítható meg" — not a text fix (the read needs another path). | **NARROWED — the LAN/gateway read only; owner: CC** **2026-10-06 night: confirmed not a controller-only fix** — the only host-network container is samba (`infra/samba.go:123`) and the agent's local API has no guest-network route; the fix is a new agent route (e.g. the PVE lxc interfaces read) + a controller reader — a two-repo design. | — | — | CC | | **R-285** | Monitoring & notifications | P4 | **A planned, supervised reinstall pages the operator as if the machine had died — there is no notion of expected downtime anywhere.** During the 2026-08-09 rehearsal the hub sent, all `status: sent` to the operator channel: `host_stale` 08:58 UTC, `node_stale` 09:00, **`host_down` 09:28 (error)**, **`node_down` 09:30 (error)**, `host_leaf_changed` 09:31, `host_recovered` 09:31, `node_recovered` 09:34, `offsite_delivery_stuck` 09:34 — eight operator mails for work that was deliberate, attended and announced. **This is the OPPOSITE gap from the one R-281 filed:** the alarms are not missing, they are indiscriminate. `host_stale` at 30 min and `host_down` at 60 min (`monitor/host_staleness.go:22-23`, `downAfter = 2 * threshold`) cannot distinguish a wiped-on-purpose box from a dead one, and `host_leaf_changed` firing on a reinstall is correct-but-expected. **Note the interaction with the mute used on 2026-08-09 evening:** blocking a customer silences everything, so today the only two settings are *page me for planned work* and *tell me nothing at all*. **What is owed is a middle:** a maintenance window, or an operator-set expected-downtime flag, that suppresses staleness and leaf-change while leaving genuine faults audible | **READY (M) — NEW 2026-08-09** | — | The evidence is the operator's mailbox plus `events`/`notification_log` for 2026-08-09 | CC | | **R-1005** | Monitoring & notifications | P4 | **A recovery mail says how long the box was „stale", not how long it was gone: after a 51-minute power cut demo-hp's `host_recovered` read „was stale for 8m" and `node_recovered` „23m".** An operator reading only the recovery mail under-reads the outage (`audits/night-power-2026-10-13/` R6). Same kind, the other way: the off-site mail of 2026-10-14 02:18 was titled „3 of 3 apps failed to back up (offsite run)" while 11 other apps were backed up in that run — only the 3 flagged apps were counted. | **READY** — filed 2026-10-14 | — | CC: add the time since the last report before the gap to both recovery messages, and count every app of the run in the off-site subject, with a test that asserts each text; next hub release | CC | -| **R-1049** | Monitoring & notifications | P3 | **A pulled drive mails the operator „unexpectedly disconnected” (error), and nothing ever mails that it came back.** MEASURED 2026-10-14 night (`audits/media-drive-events-2026-10-14/K8-hub-mails.txt`, the hub DB with its WAL from the running pod, and the mailbox as the second channel): two pulls → two `storage_disconnected` mails; the hub RECEIVED both `storage_reconnected` and both `health_recovered` (severity info) and logged no notification for any of them — they are not in `recoveredPairedDownTypes` (`hub/internal/notify/dispatcher.go`), so the severity gate drops them. The R-97a shape: told it broke, never told it healed — the operator cannot tell a returned drive from one still gone without opening the hub. | **READY** — filed 2026-10-14 night | — | Add `storage_reconnected` → `storage_disconnected` (and `health_recovered` → `health_degraded`, if the down mailed) to the recovery pairs; a test that the pair mails both edges | CC | +| **R-1049** | Monitoring & notifications | P3 | **A pulled drive mails the operator „unexpectedly disconnected” (error), and nothing ever mails that it came back.** MEASURED 2026-10-14 night (`audits/media-drive-events-2026-10-14/K8-hub-mails.txt`, the hub DB with its WAL from the running pod, and the mailbox as the second channel): two pulls → two `storage_disconnected` mails; the hub RECEIVED both `storage_reconnected` and both `health_recovered` (severity info) and logged no notification for any of them — they are not in `recoveredPairedDownTypes` (`hub/internal/notify/dispatcher.go`), so the severity gate drops them. The R-97a shape: told it broke, never told it healed — the operator cannot tell a returned drive from one still gone without opening the hub. **BUILT 2026-10-15 — hub v0.151.0 (`12552aca`): both pairs in `recoveredPairedDownTypes`, cooldowns ended by the next loss (`TestR1049_DriveBackMailsBothEdges`, red-proved). Not deployed: the operator's „yes” first (decision 162); the image build waits for it too.** | **WAITING-ON-OPERATOR** — hub v0.151.0 code pushed, not built, not deployed | the operator's „yes” to the hub deploy | On the „yes”: build + deploy hub v0.151.0; read a „came back” mail on the next pull | CC | ## Hub & operator — 12 rows (P3 7, P4 5) @@ -308,7 +299,7 @@ stopping line that lies. |---|---|---|---|---|---|---|---| | **R-992** | Process & tooling | P3 | **The drill-catalog route that `09` §6.5 documents cannot be followed: `admin/app-catalog-drill` is PRIVATE and no box has catalog credentials.** Measured 2026-10-12 while publishing the Usenet pair: the box's clone fails with `fatal: could not read Username for 'https://gitea.dooplex.hu'` (exit 128), the Gitea API reports the drill repo `private=true` and the live catalog `private=false`, and NEITHER the current controller.yaml on 9202 NOR any of its 30 saved copies has ever carried `git.username` or `git.token` (the controller supports both — `internal/config/config.go` GitConfig). The route worked for the campaigns of 2026-10-10 and the torrent pair earlier the same day, so the repository's visibility changed between then and now. **Worked around, not fixed:** that campaign staged its two templates in the box's own catalog cache (`audits/usenet-2026-10-12/tools/stage_in_cache.py`), which is where the sync copies a catalog change anyway, with the sync interval raised to 12h for the duration and rescan used in place of sync. The real fix is an operator decision: either a read-only catalog credential on the scratch box (a secret on a Tier-0 guest) or making the drill repository readable. Until then every new-app campaign hits this. | WAITING-ON-OPERATOR | the operator's choice between a box credential and a readable drill repo | Decide; then either set `git.username`/`git.token` on 9202 from the credentials store, or flip the drill repo, and correct `09` §6.5 either way | operator | | **R-1017** | Process & tooling | P3 | **A comment names a guard test that does not exist.** `felhom-controller/controller/internal/appbackup/paths.go:58` says „`TestNoBareDrivePathToUserdataDir` is the guard that keeps the count from growing”; no test of that name (or `NoBareDrive*`) exists in the repo (grep 2026-10-14; positive control: `TestRemoveStack_R800_UserdataKeptIsNamed` is found). A comment asserting an invariant with no test pinning it — the class in the `felhom-testing` skill. | **READY** — filed 2026-10-14 | — | Write the guard (a test that fails when a bare drive path reaches `UserdataDir`), red-proof it, or correct the comment | CC | -| **R-1022** | Process & tooling | P3 | **Tester 1 follows the drill catalog and carries two test drives — teardown owed (operator ruling (e), 2026-10-14).** Steps, only on the operator's word: (1) remove the test apps (Sonarr, Radarr, qBittorrent, Transmission, Plex, Jellyfin) through the dashboard WITHOUT „delete data” (R-1027); (2) decommission Media A and Media B through Tester 1's dashboard; (3) `repoint.py restore` — `controller.yaml` from `controller.yaml.pre-media-2026-10-14`, read back byte-identical, `repo_url` the live catalog; remove `drill-catalog.git` from the controller volume; (4) on demo-hp: `qm set 341 --delete scsi2,scsi3`, then delete the two volumes BY NAME (`nvme-scratch:341/vm-341-disk-3.raw`, `-disk-4.raw`) — never `qm destroy`; (5) VM memory unchanged (8192, not raised); (6) drill repo reset to live `main`. Tools: `audits/media-all-drives-2026-10-14/``tools/`. **Added 2026-10-14 night:** (4b) Media C — `qm set 341 --delete scsi4`, then the volume `nvme-scratch:341/vm-341-disk-5.raw` by name (decommission it on the dashboard first; it holds 4 test files); (7) DONE 2026-10-15: `plex-k6` and its volume removed after the claim (0 left) — the operator removes its server entry „plex-k6” from plex.tv (Settings → Authorized Devices); (8) drill commit `925f3fd` (media_c binds) goes with step 6; Prowlarr and the test drive tag „driva-a” go with step 1. Tools also in `audits/media-drive-events-2026-10-14/tools/`. | **WAITING-ON-OPERATOR** — filed 2026-10-14 | the operator's teardown word | Run the six steps; quote each read-back | CC | +| **R-1022** | Process & tooling | P3 | **Tester 1 follows the drill catalog and carries two test drives — teardown owed (operator ruling (e), 2026-10-14).** Steps, only on the operator's word: (1) remove the test apps (Sonarr, Radarr, qBittorrent, Transmission, Plex, Jellyfin) through the dashboard WITHOUT „delete data” (R-1027); (2) decommission Media A and Media B through Tester 1's dashboard; (3) `repoint.py restore` — `controller.yaml` from `controller.yaml.pre-media-2026-10-14`, read back byte-identical, `repo_url` the live catalog; remove `drill-catalog.git` from the controller volume; (4) on demo-hp: `qm set 341 --delete scsi2,scsi3`, then delete the two volumes BY NAME (`nvme-scratch:341/vm-341-disk-3.raw`, `-disk-4.raw`) — never `qm destroy`; (5) VM memory unchanged (8192, not raised); (6) drill repo reset to live `main`. Tools: `audits/media-all-drives-2026-10-14/``tools/`. **Added 2026-10-14 night:** (4b) Media C — `qm set 341 --delete scsi4`, then the volume `nvme-scratch:341/vm-341-disk-5.raw` by name (decommission it on the dashboard first; it holds 4 test files); (7) DONE 2026-10-15: `plex-k6` and its volume removed after the claim (0 left) — the operator removes its server entry „plex-k6” from plex.tv (Settings → Authorized Devices); (8) drill commit `925f3fd` (media_c binds) goes with step 6; Prowlarr and the test drive tag „driva-a” go with step 1. Tools also in `audits/media-drive-events-2026-10-14/tools/`. **Added 2026-10-15 (media build):** (4c) Media D — decommission it on the dashboard, then `qm set 341 --delete scsi5` and the volume `nvme-scratch:341/vm-341-disk-6.raw` by name (20 GB; 2 own-bytes torrents on it); (9) Tester 1 runs controller `0.313.0-rc.3/rc.4` by hand — put the vouched release back (`/etc/felhom-controller-image`); (10) the rig's Sonarr/Radarr hold two sets of drive tags and clients (the night script's `drive-a…` and the product's `media-a…`) — they go with step 1. | **WAITING-ON-OPERATOR** — filed 2026-10-14 | the operator's teardown word | Run the six steps; quote each read-back | CC | | **R-1034** | Process & tooling | P3 | **Gate 17 checks the FAQ JSON-LD on two pages only, by a hard-coded list — a FAQPage anywhere else is unchecked.** `scripts/site_gates.py` gate 17 loops `for p in ("gyik.html", os.path.join("en", "faq.html"))`, so its „JSON-LD equals the visible questions and answers” guarantee stops at those two files. **Since 2026-10-14 two more pages carry a FAQPage block the gate never reads:** `/sajat-felho-tarhely` and `/en/own-cloud-storage`. Their JSON-LD was *derived* from the visible headings and paragraphs by a script and then verified verbatim against the rendered text, so it is correct today — but nothing stops the next copy edit moving the visible text and leaving the structured data behind, which is exactly the failure gate 17 exists to catch, and the one Google penalises. **Why it is not a one-line fix:** gate 17's extractor is written for the FAQ pages' accordion markup (`