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
gates / gates (push) Successful in 6m34s
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user