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
@@ -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.