Media all drives, Part D: test sheet for the operator; design note 15 (draft); 09 §3 decisions 237-241; R-1022..R-1027 (removal deletes the shared library R-1027, restart restarts nothing R-1026)
gates / gates (push) Successful in 6m34s

This commit is contained in:
2026-10-14 17:52:56 +02:00
parent 170d090e7b
commit e6dbc7304b
9 changed files with 393 additions and 7 deletions
+21 -2
View File
@@ -4,7 +4,13 @@
> sessions in this repo clobber each other") sends a parallel session to a topic file. The task asked for the line at
> the top of `REPORT.md`; it is here instead.
**Resume point:** Parts A–C done and pushed. Next: Part D (Plex path check, the test sheet, then STOP for the operator).
**Resume point (STOP, Part D):** A–D done and pushed; the test sheet went to the operator. Waiting for „done". Then:
Part E (`tools/where.py`, then S5 pull drive B = `qm set 341 --delete scsi3` on demo-hp, S6 re-attach), then finish
`architecture/15-media-library.md` §5 and this report. Tester 1 follows the drill mirror (drill `3297003`).
**Read this before the live fix:** `media/` also holds `photos`, `books`, `music` — binding it whole gives the download
apps write access to them (`15` §2, operator decision D9). And removal with „delete data" deletes literal binds under
`HDD_PATH` (R-1027) — the live fix must not use plain host paths.
# Media apps on every drive — Tester 1 rig (2026-10-14)
@@ -30,4 +36,17 @@ Evidence: `documentation/audits/media-all-drives-2026-10-14/README.md` (one row
## Register
Rows before 133 → after 139 · opened 6 (R-1014..R-1019) · closed 0.
Rows before 133 → after 146 (another session added R-1020, R-1021 and closed R-1007 meanwhile) · opened 12 (R-1014..R-1027, R-1022..R-1026) · closed 0.
## Part D (Plex, the sheet)
- Plex behind the tunnel: the drill template now sets `ADVERTISE_IP=https://plex.<domain>:443/`. A throwaway unclaimed
Plex read it back as `customConnections`; public `/identity` and `/web` 200; anonymous library calls 401. Removed.
- **The removal of that Plex deleted drive A's whole `userdata/media` (80 KB empty skeleton)** — R-1027. Rebuilt by the
controller's file-browser sync; `torrents/` and `transmission/` re-made by hand with the skeleton's owner and mode
(the drill templates no longer declare them); the four apps stopped and started through the dashboard.
- **The dashboard's „restart" restarts nothing** (R-1026) — found while repairing.
- Decisions in `09` §3: 237–241 (the operator's rulings of 2026-10-14, recorded).
- Design note `architecture/15-media-library.md` — DRAFT, §5 pending S5/S6. Listed in `documentation/README.md`.
- No CHANGELOG entry: `felhom.eu`'s CHANGELOGs are per area (hub, scripts, website, marketing); this session changed
none of them.
+3
View File
@@ -22,6 +22,7 @@
## What we work on
- The power-cut findings still open (R-1004, R-1005, R-1010, R-1012, R-1013); the hub build must refuse a stale checkout (R-1021).
- Media apps on every drive (R-1023): the layout is proven on Tester 1 — a download reaches the library as one file, no copy. The live templates get fixed next (R-1014).
## Waiting on you — decide
@@ -35,10 +36,12 @@
| D6 | R-992 | The scratch box cannot read the private drill catalog: give it a read-only login, or make that copy readable? | **A read-only login** | Readable copy: the drill catalog becomes public | every new-app test needs hand-staging | new-app tests |
| D7 | R-990, R-989 | Rule yes / no / later on the forum ideas (`audits/forum-research-2026-10-12/FELHOM.md` §5) and the selfh.st ideas (`audits/selfhst-survey-2026-10-12/FELHOM.md` §6) | — | — | nothing changes; the research ages | — |
| D8 | R-206 | Install DooPlex's Docker settings from git (restarts Docker), and prune the build cache weekly: yes or no? | **Yes, in a quiet window** | — | a rebuild of DooPlex still has the settings in git | — |
| D9 | R-1023 | Media apps: bind the whole `media` folder (download apps can then write into Photos and Music), or move films and shows to their own folder first? | **Own folder** (`15-media-library.md` §2) | Whole `media`: no move, but the internet-facing apps reach the family photos | the build waits for this answer | the build |
## Waiting on you — do
- **Controller 0.311.0 to the N100 and Tester 1:** bake golden 0.311.0 (CC, ~30 min), vouch it, then the global floor 0.311.0 (MinAgent 0.155.0) and clear demo-hp's own floor. If not: only demo-hp repairs Immich after a power cut; demo-hp's own floor stays above the golden.
- **The shared `git/felhom.eu` checkout** is behind and its pull fails on untracked leftovers (R-1021): say whose they are, or let CC move them aside. If not: the hub build script builds stale code.
- **The media test on Tester 1** (about one hour): `documentation/audits/media-all-drives-2026-10-14/TEST-SHEET.md`, then write „done". Then the teardown word when you want (R-1022).
- **N100 BIOS:** read „After Power Loss" (the HP's „Previous State" made it start after 8 of 8 cuts). **HP BIOS:** „Bypass F1 Prompt on Configuration Changes" → Enable.
- **Pull the failed disk** Z6A07P2G (R-934; the twin Z6A070XZ is `/mnt/3_hdd`). If not: its alarms return on 11 November.
- **Hub master key** into the password manager, then `shred -u` the key file (security review 2026-10-12, in the history).
+3
View File
@@ -40,6 +40,9 @@ The operator-tier agent and the Proxmox platform.
### Hub (operator backend) — `architecture/05`
- [`architecture/05-hub-architecture.md`](architecture/05-hub-architecture.md) — hub architecture (v0.11.0)
### Apps — `architecture/15`
- [`architecture/15-media-library.md`](architecture/15-media-library.md) — the media library: media apps on every drive, the links, the restart notice (**DRAFT**, 2026-10-14)
### Security audits & remediation — `audits/`
- [`audits/deep-sweep-2026-06-13.md`](audits/deep-sweep-2026-06-13.md) — cross-repo deep audit (controller + agent) with remediation status
- [`audits/bughunt-reconcile-2026-06-13.md`](audits/bughunt-reconcile-2026-06-13.md) — reconciliation of the v0.30.3 BUGHUNT against current code + merged fix list
@@ -915,6 +915,20 @@ its length, and both fixes cost something the household would notice — operato
127. **The agent's three by-design abilities (`03` §3.1) stay for now**; revisited before the first paying customer.
*Operator ruling 2026-10-05.* (R-861)
### 2026-10-14 (afternoon) — media apps on every drive (recorded before the work; the media-all-drives brief)
237. **Media apps see every drive.** Each media app (`15-media-library.md` §3) binds every enrolled drive's media
library, not only the drive picked at install. *Operator ruling 2026-10-14.* (R-1023)
238. **After a drive is added or removed, the box says so.** It tells the household that the media apps will not see
the change until they restart, and offers „restart now" or „schedule for later". *Operator ruling 2026-10-14.*
(R-1023; today's restart button restarts nothing — R-1026)
239. **Each drive shows whether its apps have restarted since it was added.** If they have not, a manual restart
button is there. *Operator ruling 2026-10-14.* (R-1023)
240. **The N100 becomes a lived-in box.** Its rule is recorded by the second task of 2026-10-14
(`TASK-media-folder-fix-and-n100-2026-10-14.md`). *Operator ruling 2026-10-14.*
241. **Tester 1 may follow the drill catalog for the media test, until the operator says „tear it down".** It is the
one exception to §6.5's fence; the teardown is R-1022. *Operator ruling 2026-10-14.*
### 2026-10-14 — R-1007: how the box asks Immich to repair after a power cut (recorded before the build)
236. **The box repairs Immich's broken photos through Immich's OWN job queue (option 1, spike A4)** — the controller adds
@@ -2389,7 +2403,7 @@ is fixed, the drill procedure is:
### The fence
Only a scratch guest is ever pointed at the drill catalog. The drill repo's README says so, and no
Only a scratch guest is ever pointed at the drill catalog. **Exception: Tester 1, for the media test, until the operator's teardown word — §3 decision 241.** The drill repo's README says so, and no
customer box has credentials for it. The store listens on the guest's loopback only.
## 7. What slices 1 and 2 actually built
@@ -0,0 +1,135 @@
# 15 — The media library: media apps on every drive
> **What this is:** how the media apps (download → library → player) share the household's drives — the folder
> layout, the links between the apps, what happens when a drive comes or goes, and what backup, removal and export must
> 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** — §5 waits on S5/S6 (after the operator's test).
## 1. The problem
**[FACT]** Live catalog (`main` 8ee687e): qBittorrent saves to `${USERDATA_PATH}/media/torrents`, Transmission to
`media/transmission`; Sonarr and Radarr look in `${USERDATA_PATH}/downloads`. A download never reaches a library.
**[FACT]** Even with matching folders, Sonarr/Radarr bind `media/tv` (or `movies`) and `downloads` as TWO bind mounts:
`link(2)` answers EXDEV across two mount points on one filesystem, so every import is a full copy (S1:
`ln: … Cross-device link`; two inodes). **[FACT]** Each app binds ONE drive (`HDD_PATH`, locked after deploy,
`stacks/deploy.go` `withPathVars`); only FileBrowser follows every drive (`web/handlers.go` `buildFileBrowserPaths`).
## 2. The layout [DESIGN, measured on Tester 1]
Each media app binds, **per enrolled drive**, that drive's `userdata/media` at **the same path inside the container as
in the guest**:
```
/mnt/felhom-drives/<drive>/userdata/media → /mnt/felhom-drives/<drive>/userdata/media (rw; :ro for players)
media/tv/<Show>/… Sonarr library media/torrents/{tv,movies}/… qBittorrent
media/movies/<Film>/… Radarr library media/transmission/{tv,movies}/… Transmission
```
The four reasons:
1. **One bind per drive keeps download and library on one mount point** — the import is a hardlink: one file, two
names, no extra space. **[FACT]** S2: Transmission and qBittorrent, drive B → link count 2, same inode; S3: drive A
the same.
2. **The same path everywhere** — no app translates another app's path (no „remote path mapping"), and moving an
installed app from one drive to many later ADDS binds without rewriting the paths Sonarr and Radarr have stored.
3. **`media/` only** — never `felhom-data`, never the whole `userdata` (documents, roms and the app's own data stay
out).
4. **Today's host folders** (`media/torrents`, `media/transmission`) — an installed download app keeps its files.
**[FACT, and a correction of the brief]** `media/` is not photo-free: the skeleton puts `photos`, `books`,
`audiobooks`, `music`, `comics`, `podcasts` there too (`appbackup/userdata.go` + the catalog-derived set), and Immich,
calibre-web, audiobookshelf and navidrome use them. With reason 1, a download app needs ONE mount that holds both its
download folder and the libraries — so binding `media/tv` and `media/torrents` separately brings EXDEV back. **Open
question for the operator (two options):**
- **A — bind `media/` whole, rw for the download apps.** Simple; measured working. Cost: qBittorrent, Transmission,
Sonarr and Radarr can write into the family's photos and music folders.
- **B — move the video library into its own folder** (e.g. `userdata/video/{tv,movies,torrents,transmission}`) and bind
only that. Download apps never see photos. Cost: a one-time move on boxes that already have files there, and every
template and skeleton entry changes.
- **CC recommends B** for the build: the download apps are the internet-facing ones, and „the box cannot touch your
photos" is a promise worth one move. A is fine for the Tester 1 rig.
## 3. Which apps are „media apps" [DESIGN, proposal]
Sonarr, Radarr, qBittorrent, Transmission (download/library, rw); Plex, Jellyfin, Emby (players, `:ro`). Others
(Lidarr, Readarr, SABnzbd, NZBGet, navidrome, audiobookshelf…) later, each with its own rule. A media app carries a
marker in `.felhom.yml` (e.g. `media_library: rw|ro`); the controller builds its per-drive binds — templates never
spell drive names (the drill used plain host paths only to stay out of the classifier, §7).
## 4. The drive-absent gate and the restart notice
**[FACT]** FileBrowser's pattern is the one to extend: bind every drive's `userdata/`, skip a drive that is not a live
mountpoint, recreate the container when the set changes. **[FACT]** The app gate (`web/intermediary.go`
`planDriveGates`/`appsOnStoragePath`) stops and restarts only apps whose `HDD_PATH` equals the absent drive; at boot it
waits for THAT drive's bind only (seen after the 17:25 power cut: the four media apps were recreated onto
`media_a` with no check of `media_b`). A media app depends on EVERY drive it binds.
**[FACT] What the dashboard says when a drive is added (A5):** the drive card „Aktív", „Nincs alkalmazás ezen a
tárolón" — nothing about apps that need a restart. **[FACT] Docker binds are resolved at container START**: a running
container does not see a drive added later (guest propagation is `shared,slave`, the container's bind is `rprivate`).
**[FACT] Today's „restart" restarts nothing** (R-1026): `RestartStack` = `compose up -d`, a no-op when the compose did
not change; it logged success in 0.1 s while the containers kept their start time. Stop + start works. The notice of
decisions 238–239 needs a real restart (`up -d --force-recreate` once the per-drive binds are in the compose).
**[DESIGN]** The restart notice (decision 238) fires on: a drive added, a drive removed, and — pending S6 — a drive
that came back. Each drive card carries „apps restarted since this drive was added: yes/no" with the button
(decision 239).
## 5. A drive is pulled / comes back — PENDING (S5/S6, after the operator's test)
Predictions written: a pulled drive B stops nothing (no media app has `HDD_PATH` on B); the apps keep a dead view of B;
Plex/Jellyfin may mark items unavailable; on return, running containers do not see B until recreated.
## 6. The links [FACT, measured calls in `audits/media-all-drives-2026-10-14/tools/wire.py`]
Per drive: a qBittorrent category `tv-<d>`/`movies-<d>` (save path on that drive, incomplete path on that drive); a
Sonarr/Radarr root folder, a tag, and two client entries („qBittorrent <d>" by category, „Transmission <d>" by
directory) with that tag. Facts the link code needs:
- **The API key**: read from `/config/config.xml` — or SET at install with `SONARR__AUTH__APIKEY` /
`RADARR__AUTH__APIKEY` (measured: 200 with that key, 401 with another). Setting it is the cleaner design.
- **qBittorrent must run in automatic torrent management** (`auto_tmm_enabled`), or a category's save path is ignored
and every torrent lands on the default drive.
- **A Transmission entry must clear the default category** — Sonarr/Radarr refuse „Cannot use Category and Directory".
- **Transmission has ONE incomplete folder** for all torrents; it must be off, or every drive-B download is a cross-drive
copy at completion.
- **Transmission answers by its container name** (`transmission:9091`) despite its Host allow-list (measured).
- **A show with no tag gets no download app** when every entry carries a tag. The UI must make the drive choice set
both the root folder AND the tag; S3: a mismatch (root B, tag A) works but makes a full copy on B while A seeds.
- **The OnlyOffice revoke-on-stop rule must NOT apply** (`integrations/lifecycle.go:10-13`): it would delete the download
entries whenever a download app restarts. And the OnlyOffice links themselves have no live proof since Proxmox
(R-1018).
- Plex and Jellyfin are NOT linked: they watch folders. Indexers stay the household's choice.
- **Plex behind the tunnel** needs `ADVERTISE_IP=https://<sub>.<domain>:443/` so plex.tv hands apps a reachable
address (measured: `customConnections` set; public `/identity` 200; anonymous library calls 401).
## 7. Backup classes for a per-drive media bind
**[FACT]** An unlisted writable `${…_PATH}` bind is MANDATORY (every tier, off-site included); a plain host path is in
no capture set (`stacks/classify_binds.go` `classifyRoot`). Measured over the live templates: Transmission's unlisted
`watch` bind goes off-site; the download folders are `optional`, so a second drive gets a nightly copy of every
download (R-1019). **[DESIGN]** The per-drive media bind is declared `excluded` for every media app — the family's
media is no single app's data; whether the library gets its own household-level backup is a separate decision.
## 8. Removal and export assume one drive per app
**[FACT]** Removal with „delete data" deletes every bind under the app's `HDD_PATH`, literal and `:ro` ones included —
removing a reader Plex deleted drive A's whole `userdata/media` (R-1027). **[DESIGN]** A media app's removal never
deletes the shared library; it deletes only its own config.
Call sites that assume ONE `HDD_PATH`/`USERDATA_PATH` per app (controller main acbbf4b):
`stacks/deploy.go` `withPathVars`; `stacks/manager.go:1566`; `stacks/delete.go` `appHDDPath`, `hddPathForRemoval`,
`ExportDataMounts`, `ParseComposeUserdataMounts`; `stacks/kept.go:226`; `stacks/migrate.go` `MigrateApp` (rewrites
`HDD_PATH`); `web/intermediary.go` `appsOnStoragePath`; `web/handlers.go` `appsUsingPath`; `backup/file_legs.go`,
`backup/backup.go:431`, `backup/offbox_capture.go`, `backup/offbox_placement.go`, `backup/offbox_restore.go`;
`report/dr_recipe.go:110`; `appexport/provider.go`.
## 9. From one drive to many
An installed media app keeps its `HDD_PATH` (its config's unit lives there) and gains binds for the other drives at its
next recreate. Because the paths are the same everywhere (§2 reason 2), nothing Sonarr or Radarr stored changes. A
household's existing files under `media/torrents` stay where they are.
@@ -0,0 +1,107 @@
== media_a
ls: cannot access '/mnt/felhom-drives/media_a/userdata/media': No such file or directory
/mnt/felhom-drives/media_a/userdata/:
total 20
drwxrwsr-x 5 root 1000 4096 Oct 14 15:41 .
drwxr-xr-x 4 root root 4096 Oct 14 14:09 ..
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 documents
drwxrwsr-x 2 root 1000 4096 Oct 14 14:08 downloads
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 roms
== media_b
/mnt/felhom-drives/media_b/userdata/:
total 24
drwxrwsr-x 6 root 1000 4096 Oct 14 13:59 .
drwxr-xr-x 3 root root 4096 Oct 14 13:59 ..
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 documents
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 downloads
drwxrwsr-x 16 root 1000 4096 Oct 14 13:59 media
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 roms
/mnt/felhom-drives/media_b/userdata/media:
total 64
drwxrwsr-x 16 root 1000 4096 Oct 14 13:59 .
drwxrwsr-x 6 root 1000 4096 Oct 14 13:59 ..
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 audiobooks
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 books
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 comics
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 grimmory
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 metube
drwxrwsr-x 2 root 1000 4096 Oct 14 15:33 movies
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 music
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 nzbget
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 photos
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 podcasts
drwxrwsr-x 4 root 1000 4096 Oct 14 15:03 torrents
drwxrwsr-x 5 root 1000 4096 Oct 14 14:56 transmission
drwxrwsr-x 2 root 1000 4096 Oct 14 14:10 tv
drwxrwsr-x 2 root 1000 4096 Oct 14 13:59 usenet
2026/10/14 15:38:14 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/paperless-ngx/docker-compose.yml
2026/10/14 15:38:14 [INFO] [stacks] ParseComposeHDDMounts: found 0 HDD mounts for /opt/docker/stacks/privatebin/docker-compose.yml
2026/10/14 15:38:14 [INFO] [stacks] ParseComposeHDDMounts: found 0 HDD mounts for /opt/docker/stacks/privatebin/docker-compose.yml
2026/10/14 15:38:14 [INFO] [stacks] ParseComposeHDDMounts: found 0 HDD mounts for /opt/docker/stacks/privatebin/docker-compose.yml
2026/10/14 15:38:14 [INFO] [stacks] ParseComposeHDDMounts: found 1 HDD mounts for /opt/docker/stacks/radarr/docker-compose.yml
2026/10/14 15:38:14 [INFO] [stacks] ParseComposeHDDMounts: found 1 HDD mounts for /opt/docker/stacks/sonarr/docker-compose.yml
2026/10/14 15:38:14 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/transmission/docker-compose.yml
2026/10/14 15:38:14 [INFO] [stacks] ParseComposeHDDMounts: found 1 HDD mounts for /opt/docker/stacks/qbittorrent/docker-compose.yml
2026/10/14 15:39:54 [INFO] [stacks] ParseComposeHDDMounts: found 1 HDD mounts for /opt/docker/stacks/plex/docker-compose.yml
2026/10/14 15:39:54 [INFO] [stacks] ParseComposeHDDMounts: found 1 HDD mounts for /opt/docker/stacks/plex/docker-compose.yml
2026/10/14 15:39:54 [INFO] [stacks] Deploying stack plex with 4 env vars: [DOMAIN, SUBDOMAIN, PLEX_CLAIM, HDD_PATH]
2026/10/14 15:41:23 [INFO] [api] Remove requested for stack: plex
2026/10/14 15:41:23 [INFO] Removing deployed stack: plex (removeHDDData=true, hddDeclared=true, backupPaths=2)
2026/10/14 15:41:23 [INFO] [stacks] ParseComposeHDDMounts: found 1 HDD mounts for /opt/docker/stacks/plex/docker-compose.yml
2026/10/14 15:41:50 [INFO] [stacks] RemoveStack plex: removed volume(s) [plex_plex_config plex_plex_transcode]
2026/10/14 15:41:50 [INFO] Removed HDD data: /mnt/felhom-drives/media_a/userdata/media (80K)
2026/10/14 15:41:50 [INFO] Stack plex removed successfully (took 26.7s)
2026/10/14 15:41:50 [INFO] Event pushed: app_removed (info) [hu-only] — Alkalmazás eltávolítva: plex
2026/10/14 15:41:50 [INFO] [stacks] image retention (remove of plex): deleted [plexinc/pms-docker:1.41.4.9463-630c9f557] (0186be6d1305, 351MB) — no container, installed app or undo names it (decision 53)
2026/10/14 15:41:50 [INFO] [stacks] image retention (remove of plex): pass over 21 image(s) of [plexinc/pms-docker] — 1 candidate(s), 1 deleted, the rest kept
drwxrwsr-x 14 root 1000 4096 Oct 14 15:42 /mnt/felhom-drives/media_a/userdata/media
audiobooks books comics grimmory metube movies music nzbget photos podcasts tv usenet
sonarr: 2026-10-14T15:28:50.432279316Z sees A=0
radarr: 2026-10-14T15:28:45.399705005Z sees A=0
qbittorrent: 2026-10-14T15:29:00.502333441Z sees A=0
transmission: 2026-10-14T15:28:58.010876451Z sees A=0
drwxrwsr-x 2 root 1000 4096 Oct 14 15:44 torrents
drwxrwsr-x 3 root 1000 4096 Oct 14 15:44 transmission
17:44:33 qbittorrent restart -> 200 {'ok': True, 'data': {'state': 'running'}, 'message': 'Stack qbittorrent restart
17:44:33 transmission restart -> 200 {'ok': True, 'data': {'state': 'running'}, 'message': 'Stack transmission restar
17:44:33 sonarr restart -> 200 {'ok': True, 'data': {'state': 'running'}, 'message': 'Stack sonarr restart requ
17:44:33 radarr restart -> 200 {'ok': True, 'data': {'state': 'running'}, 'message': 'Stack radarr restart requ
sonarr: started 2026-10-14T15:28:50.432279316Z sees A=0 B=14
radarr: started 2026-10-14T15:28:45.399705005Z sees A=0 B=14
qbittorrent: started 2026-10-14T15:29:00.502333441Z sees A=0 B=14
transmission: started 2026-10-14T15:28:58.010876451Z sees A=0 B=14
sonarr: started 2026-10-14T15:28:50.432279316Z sees A=0
radarr: started 2026-10-14T15:28:45.399705005Z sees A=0
qbittorrent: started 2026-10-14T15:29:00.502333441Z sees A=0
transmission: started 2026-10-14T15:28:58.010876451Z sees A=0
2026/10/14 15:44:32 [INFO] [api] restart requested for stack: qbittorrent
2026/10/14 15:44:32 [INFO] [stacks] Restarting stack: qbittorrent
2026/10/14 15:44:32 [INFO] [stacks] Stack qbittorrent restarted successfully (took 0.1s)
2026/10/14 15:44:33 [INFO] [api] restart requested for stack: transmission
2026/10/14 15:44:33 [INFO] [stacks] Restarting stack: transmission
2026/10/14 15:44:33 [INFO] [stacks] Stack transmission restarted successfully (took 0.1s)
2026/10/14 15:44:33 [INFO] [api] restart requested for stack: sonarr
2026/10/14 15:44:33 [INFO] [stacks] Restarting stack: sonarr
2026/10/14 15:44:33 [INFO] [stacks] Stack sonarr restarted successfully (took 0.1s)
2026/10/14 15:44:33 [INFO] [api] restart requested for stack: radarr
2026/10/14 15:44:33 [INFO] [stacks] Restarting stack: radarr
2026/10/14 15:44:33 [INFO] [stacks] Stack radarr restarted successfully (took 0.1s)
17:46:00 qbittorrent stop -> 200
17:46:05 transmission stop -> 200
17:46:08 sonarr stop -> 200
17:46:12 radarr stop -> 200
17:46:32 qbittorrent start -> 200 {'ok': True, 'data': {'state': 'starting'}, 'message': 'Stac
17:46:33 transmission start -> 200 {'ok': True, 'data': {'state': 'starting'}, 'message': 'Stac
17:46:34 sonarr start -> 200 {'ok': True, 'data': {'state': 'starting'}, 'message': 'Stac
17:46:34 radarr start -> 200 {'ok': True, 'data': {'state': 'starting'}, 'message': 'Stac
sonarr: started 2026-10-14T15:46:33.691456Z healthy sees A=14 B=14
radarr: started 2026-10-14T15:46:34.33316681Z healthy sees A=14 B=14
qbittorrent: started 2026-10-14T15:46:32.525118942Z healthy sees A=14 B=14
transmission: started 2026-10-14T15:46:33.100564276Z healthy sees A=14 B=14
@@ -0,0 +1,18 @@
17:39:54 deploy throwaway plex -> 202 {"ok": true, "data": {"warning": "Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát. Normál használat mellett ez nem okoz pro
17:40:19 state running
Attempting to obtain server token from claim token
ProcessedMachineIdentifier="7bcd5d93956601d9573acffac9f6ebcc2787e8d0
customConnections="https://plex.enkicsifelhom.hu:443/
public https://plex.enkicsifelhom.hu/identity -> 200
public https://plex.enkicsifelhom.hu/web/index.html -> 200
public GET /library/sections (no token) -> 401 <html><head><title>Unauthorized</title></head><body><h1>401 Unauthorized</h1></body></html>
public GET /:/prefs (no token) -> 401 <html><head><title>Unauthorized</title></head><body><h1>401 Unauthorized</h1></body></html>
public GET /myplex/account (no token) -> 401 <html><head><title>Unauthorized</title></head><body><h1>401 Unauthorized</h1></body></html>
public GET /accounts (no token) -> 401 <html><head><title>Unauthorized</title></head><body><h1>401 Unauthorized</h1></body></html>
17:41:18 [X] stop -> 200 {'ok': True, 'message': 'Stack plex stop completed'}
17:41:50 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'plex', 'volumes_removed': ['plex_plex_config', 'plex_plex_transcode'], 'hdd_paths_removed': ['/mnt/felhom-drives/media_a/userd
17:41:59 [X] after remove: deployed=False leftovers='/opt/docker/stacks/plex'
/opt/docker/stacks/plex
/mnt/felhom-drives/media_b/userdata/media/tv:
@@ -0,0 +1,81 @@
# Test sheet — media apps on two drives (Tester 1)
About one hour. From any place with a browser. Use only content you have the right to.
## 1. Addresses
| What | Address |
|---|---|
| Dashboard | https://felhom.enkicsifelhom.hu |
| Sonarr (shows) | https://sonarr.enkicsifelhom.hu |
| Radarr (films) | https://radarr.enkicsifelhom.hu |
| qBittorrent | https://torrent.enkicsifelhom.hu |
| Transmission | https://transmission.enkicsifelhom.hu |
| Plex (after step 2) | https://plex.enkicsifelhom.hu/web |
The two test drives are **Media A** and **Media B**. Their folders:
- Media A: `/mnt/felhom-drives/media_a/userdata/media/tv` and `.../media/movies`
- Media B: `/mnt/felhom-drives/media_b/userdata/media/tv` and `.../media/movies`
## 2. Plex
1. Open https://plex.tv/claim and sign in. Copy the code. It works for 4 minutes.
2. In the dashboard, open **Alkalmazások → Plex → Telepítés**.
3. Paste the code into **Plex Claim Token**. For the drive, select **Media A**. Press install.
4. Open https://plex.enkicsifelhom.hu/web. Sign in with your Plex account.
5. Make one library **TV Shows**. Add two folders: Media A `tv` and Media B `tv`.
6. Make one library **Movies**. Add two folders: Media A `movies` and Media B `movies`.
If the code expired: remove Plex in the dashboard **without** „delete data" (see 7), get a new code, install again.
## 3. Sonarr and Radarr: first sign-in
1. Open Sonarr. The box asks for your dashboard sign-in first. Then Sonarr asks you to make its admin user. Make it.
2. Do the same in Radarr.
3. In each: **Settings → Indexers → +**. Add your own download sources.
The download apps are already connected. Do not change **Settings → Download Clients**, except as step 4c says.
## 4. The test
Each show or film needs a **root folder** (which drive) and a **tag** (which download entries). The tag `drive-a` sends
the download to Media A. The tag `drive-b` sends it to Media B. **A show with no tag cannot download.**
a. **Show 1 → Media A.** Sonarr → Add New. Root folder: Media A `tv`. Tags: `drive-a`. Search for one episode.
b. **Show 2 → Media B.** Root folder: Media B `tv`. Tags: `drive-b`.
c. **Pick the download app.** Each tag has two entries: qBittorrent and Transmission. Sonarr takes turns. To force one:
**Settings → Download Clients**, open the other one (for example „Transmission drive-a"), remove the tick from
**Enable**, save. Put the tick back after the download starts. Try to use each app at least once.
d. **Show 3 → the deliberate mismatch.** Root folder: Media **B** `tv`. Tags: `drive-a`.
e. A film in Radarr, if you want. The same rules.
f. Look in Plex. The shows must appear after Sonarr imports them.
## 5. Passwords
No password is on this sheet.
- qBittorrent and Transmission: user `admin`. The password is on the app's page in the dashboard
(**Alkalmazások → the app → Megjelenítés**).
- Sonarr and Radarr: the admin you made in step 3.
## 6. The legal line
Only download content you have the right to. The download goes from the internet address of the connection this box
uses. Because of how torrents work, the other peers see that address.
## 7. Do not
- **Do not remove a media app with „delete data" / „adatok törlése".** On this test layout it deletes the whole
`media` folder of Media A (found today, R-1027). Removing without it is safe.
- Do not change the drives on the Tárhely page.
## 8. When you are done
Write **„done"** to CC. CC then reports where each file landed. Then CC removes one drive while the apps run. CC tells
you the time before.
+10 -4
View File
@@ -112,7 +112,7 @@ match what the reader sees is how an instrument stops being believed (R-421). It
**nothing was proven on the day this line was drawn** and a stopping line that moves a status is a
stopping line that lies.
## Install & onboarding — 8 rows (P3 6, P4 2)
## Install & onboarding — 7 rows (P3 5, P4 2)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -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 — 12 rows (P2 2, P3 5, P4 5)
## Apps & catalog — 14 rows (P2 2, P3 7, P4 5)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -135,6 +135,8 @@ stopping line that lies.
| **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-1015** | Apps & catalog | P3 | **Three apps tell the household to use a folder that does not exist: `/media/shows`.** Sonarr's first steps (`sonarr/.felhom.yml:74,109`), Jellyfin's (`jellyfin/.felhom.yml:77,83,112,117`) and Plex's (`plex/.felhom.yml:76,111`) name `/media/shows`; the folder every template binds and the skeleton makes is `media/tv`. Found 2026-10-14 (the task named Sonarr and Jellyfin; Plex has it too). | **READY** — filed 2026-10-14 | — | Change the copy (hu + en) to the folder the layout of `15` §2 gives; the live-fix task | 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. | **READY** — filed 2026-10-14 | — | Make restart a real restart (`up -d --force-recreate`, or stop+start), 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; the OnlyOffice revoke-on-stop rule (`lifecycle.go:10-13`) must NOT apply (it would delete the download entries at every restart). | **OPEN** — filed 2026-10-14 | R-1023 | Build with R-1023 | 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 |
@@ -186,10 +188,12 @@ 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 — 3 rows (P3 2, P4 1)
## Storage & devices — 5 rows (P2 2, 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`. 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/<library folders>` 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). | **OPEN** — filed 2026-10-14; design in `15` (S5/S6 results pending the operator's test) | the operator's test on Tester 1 („done”), then S5/S6 | Finish `15` with S5/S6; then a build task | 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 |
@@ -283,13 +287,14 @@ stopping line that lies.
| **R-989** | Business & legal | P4 | **Operator: read `documentation/audits/selfhst-survey-2026-10-12/FELHOM.md` §6 and rule on each recommendation.** READ-ONLY research 2026-10-12: the selfh.st self-hosting survey 2023–2025 (1,869 / 3,655 / 4,081 respondents, hobby users) against our catalog, features, pitch and hardware. Ten ranked recommendations, none acted on; the top three: a supported-hardware page (none exists — finding, §4), off-site backup as a *why* reason in the pitch (only 25.2 % of the crowd backs up off-site), Syncthing as the largest catalog gap (25.9 %). Tables: `TABLE.md`; method and limits: `METHOD.md`. | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages (the 2026 survey replaces it) | operator |
| **R-990** | Business & legal | P4 | **Operator: read `documentation/audits/forum-research-2026-10-12/FELHOM.md` §5 and rule on each recommendation.** READ-ONLY research 2026-10-12: three years (#7943–#11092, 2,908 posts, 195 people, 82 beginner threads) of the public PROHARDVER! topic „Home server / házi szerver építése”, tagged and counted by person (`FINDINGS.md`, method and spot-check in `METHOD.md`; no username or post text in git). Ten ranked recommendations, none acted on; the top three: a „which machine works” page (8 of 33 beginners who name their hardware own only a Synology, a router disk or a Raspberry Pi — the installer cannot run there; the same gap is R-989's first recommendation from a different source, and the shipped-page half is R-991), „you do not choose an operating system — we do” (the biggest beginner question, 20 of 82 threads), and „bring your old office PC” as the cheap entry. Catalog gap (≥ 3 people): a DLNA server (7), an ad/DNS blocker (5), a camera recorder (5). | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages | operator |
## Process & tooling — 23 rows (P3 9, P4 14)
## Process & tooling — 25 rows (P3 10, P4 15)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
| **R-1021** | Process & tooling | P3 | **`build-hub.sh` builds STALE hub code when its `git pull` fails — it only warns.** 2026-10-14: the shared checkout `git/felhom.eu` was behind `origin/main` (`52b6599d` vs `822d0393`) and its pull aborted on untracked leftovers (`audits/sizing-2026-10-13/`, `audits/usenet-2026-10-12/`, a Facebook folder, a modified `walk.py`) — the script would have shipped hub 0.149-era code labelled 0.150.0. This session built from its own clean clone instead (a copy of the script with only `REPO_DIR` changed). | **OPEN** — filed 2026-10-14 | operator: whose are the leftovers in the shared checkout | CC: make the build refuse unless the source tree is clean and `HEAD == origin/main` (the clean-tree gate, in the script); the operator or their owner cleans the shared checkout | CC |
| **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/`. | **WAITING-ON-OPERATOR** — filed 2026-10-14 | the operator's teardown word | Run the six steps; quote each read-back | CC |
| **R-987** | Process & tooling | P4 | **The 53 catalog app images have never been scanned for known vulnerabilities.** Fix: a monthly trivy pass over the images the catalog pins, reported, not gating. | OPEN | — | Add to the monthly re-test | CC |
| **R-986** | Process & tooling | P3 | **No dependency or secret scanner runs on any push, so a known-vulnerable library or a committed key is found only by a review like this one.** Proposed gate: govulncheck (called vulns only), gitleaks on the diff, trivy on our own images; start as WARN with an accepted-hits file, then FAIL. Tools and pinned versions: `audits/security-review-2026-10-12/SCANNERS.md`. Note: gitleaks' default rules did not find R-908's key. | OPEN | — | Build the gate, warn-first | CC |
| **R-733** | Process & tooling | P3 | **[P3-LOW] The test bench has NO swap and the boxes have 512 MiB — so a box proof can pass on swap where the bench fails, and nobody records whether a customer guest has swap.** MEASURED 2026-09-30 (R-732): immich's first start was OOM-killed 61–104 times on the bench (swap 0) and passed on 9202 by swapping ~108 MB; the bench given 512 MiB swap passed too. demo-hp 9201, 9202 and demo-felhom 9201 all read `swap: 512`; the golden's guest config is not recorded in its bake evidence, so a customer guest's swap is NOT measured. The harness's memory watch judges `anon` against the limit and never reads `memory.swap.current`. **Needs:** the golden's `swap` read and recorded; the box walk and the harness report `memory.swap.peak` beside `anon`; a decision whether proofs run with swap off (the stricter venue, as R-732's fix was proven). | **READY — rank P3-LOW; owner: CC (harness + golden evidence)** **2026-10-06 night: NARROWED** — the harness records each container's `swap_peak` and the venue's swap (catalog branch `night-held-2026-10-06`, `3f4611c`; SwapRecorded red-proved; bench SwapTotal 0 kB, 9202 524288 kB). LEFT: the golden's swap in its bake evidence, the box walk's record, and whether proofs run with swap off. | — | — | CC |
@@ -310,6 +315,7 @@ stopping line that lies.
| **R-786** | Process & tooling | P4 | **[P3-LOW] SparkyFitness's onboarding record has six open rows** (`app-catalog-felhom.eu/onboarding/sparkyfitness.md`): 0.5 runtime internet (food search providers), 0.7 the phone app's sign-in route through traefik, 1.6 the env names the server reads, 1.7 the entrypoint read, 5.4 a second memory watch at another limit, 8.3 no logo/screenshots on felhom.eu (404). Everything else measured this session (bench + 9202). **Needs:** each row measured, or n/a with a reason. | **READY — rank P3-LOW; owner: CC (catalog)** **Re-ranked 2026-10-03: P3→P4: onboarding record completeness; no household meets it directly.** | — | — | CC |
| **R-807** | Process & tooling | P4 | **[P3-LOW] 13 apps stay UNDETERMINED because their seeds write only to the database, so their upload / media / cache / redis volumes stay empty.** MEASURED 2026-10-02 (`audits/persistence-sweep-2026-10-02/A/sweep/TABLE.md`): claper, crafty-controller, dawarich (redis), docmost, gramps-web, immich (ML cache), outline (+redis), sparkyfitness, tandoor, vikunja, wger, wishlist, zipline — plus plex (no seed route: a plex.tv claim token) and wanderer (no fixture; unhealthy under the gate). None is BROKEN: nothing was written outside a preserved folder. **Needs:** per app, a seed that uploads one file (or the volume named n/a with a reason: a redis cache, an ML model cache). | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: test coverage; nothing was found broken.** | — | — | CC |
| **R-996** | Process & tooling | P4 | **`scripts/dooplex-offsite/test_dooplex_offsite.py` fails 28 of 30 tests when the push comes from a git WORKTREE, so the pre-push hook refuses a correct push.** MEASURED 2026-10-13 (sizing session): git runs the hook with `GIT_DIR` set; in a worktree that is an absolute path (`.git/worktrees/<name>`), and the test's own scratch repos inherit it — its `git commit` fails. Standalone: 30/30 OK; with `GIT_DIR=<worktree git dir>`: 28 errors; main clone with `GIT_DIR=.git` (relative): OK. Workaround used: push from a full clone. | **READY** — filed 2026-10-13 | — | Drop `GIT_DIR`/`GIT_WORK_TREE`/`GIT_INDEX_FILE` from the env the test's git calls use; red-proof with the absolute GIT_DIR | CC |
| **R-1025** | Process & tooling | P4 | **A power drill on demo-hp cuts Tester 1 too, and nothing warns the session using it.** 2026-10-14 17:25 CEST another session cut demo-hp's plug (`plugcut.py --plug hp`, the Immich drill) while this session ran downloads on Tester 1 (VM 341 runs on demo-hp); Tester 1 rebooted and came back by itself. The drill's fence checks Tester 1 for backups and package jobs only. | **OPEN** — filed 2026-10-14 | — | Add „a test runs on Tester 1” to the fence (a marker file the testing session writes), or name the rule in `runbooks/target-selection.md` | CC |
<!-- DUE-CHECKS-BEGIN — machine-readable. Parsed by scripts/due_checks_gate.py.
One row per dated check. The R-number must have a row above. Dates are UTC.