From 5a03fd4a577163675d92ba8db5cc8e44fba3b0bc Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Wed, 14 Oct 2026 22:01:09 +0200 Subject: [PATCH] Media folder fix: live read-back on demo-hp 9201 (hardlink, live catalog bc54c01); R-1014/1015/1016/1030 closed, R-1019 narrowed, R-1039/1040 filed; report --- REPORT-media-all-drives-2026-10-14.md | 2 + REPORT-media-folder-fix-2026-10-14.md | 61 ++++++++++++ STATUS.md | 2 +- .../tools/deploy.py | 4 +- .../C-live-cleanup-9201.txt | 13 +++ .../C-live-deploy-9201.txt | 22 +++++ .../C-live-readback-9201.txt | 27 +++++ .../tools/live_readback.py | 98 +++++++++++++++++++ documentation/backlog/CLOSED-ITEMS.md | 9 ++ documentation/backlog/OPEN-ITEMS.md | 14 ++- 10 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 REPORT-media-folder-fix-2026-10-14.md create mode 100644 documentation/audits/media-folder-fix-2026-10-14/C-live-cleanup-9201.txt create mode 100644 documentation/audits/media-folder-fix-2026-10-14/C-live-deploy-9201.txt create mode 100644 documentation/audits/media-folder-fix-2026-10-14/C-live-readback-9201.txt create mode 100644 documentation/audits/media-folder-fix-2026-10-14/tools/live_readback.py diff --git a/REPORT-media-all-drives-2026-10-14.md b/REPORT-media-all-drives-2026-10-14.md index fe3811fa..83fbe0c6 100644 --- a/REPORT-media-all-drives-2026-10-14.md +++ b/REPORT-media-all-drives-2026-10-14.md @@ -1,3 +1,5 @@ +**S1–S3 pushed; the live-fix task may start.** (Restored at the top 2026-10-14 ~21:20: the final-report edit had pushed it down. The live fix is done — `REPORT-media-folder-fix-2026-10-14.md`.) + **Parts A–E done. Waiting on the operator: the layout decision (STATUS D9) and the teardown word (R-1022).** Read before the live fix: `media/` also holds photos (`15` §2); removal deletes literal binds (R-1027, R-1032). diff --git a/REPORT-media-folder-fix-2026-10-14.md b/REPORT-media-folder-fix-2026-10-14.md new file mode 100644 index 00000000..b6ff14c8 --- /dev/null +++ b/REPORT-media-folder-fix-2026-10-14.md @@ -0,0 +1,61 @@ +# REPORT — the media folder fix in the live catalog (one drive), 2026-10-14 + +> A topic report (`CLAUDE.md`: parallel sessions do not share the root `REPORT.md`). Evidence: +> `documentation/audits/media-folder-fix-2026-10-14/`. Architecture read: `15-media-library.md`, `07` §6, +> `09` §1.2/§3; the first task's report. + +## In plain words + +1. **Does the media chain work now for a customer with one drive?** Yes. Proven on demo-hp guest 9201 with the live + catalog: Radarr and qBittorrent installed through the dashboard, one public-domain film handed to Radarr; the film in + the library is the downloaded file under a second name (link count 2, one inode). No extra space. +2. **Can the download apps reach the photos?** No. Inside qBittorrent and Transmission the `media/photos` path does not + exist; the media folder shows only their own download folder. A write to the photos path lands only in the + container's own layer; the photos on the drive stay unchanged (bench). +3. **What happens to an app that was already installed?** It gets the new template from the catalog sync and uses it + at its next start. Its old torrents keep seeding: the old `/downloads` path stays, as a second name of the same folder. + New downloads go to the same-path folder. Measured on the bench for both download apps: an install from the old + template, then updated. +4. **Backup, export, remove: unchanged in meaning?** Yes. Sonarr/Radarr's new `media` bind is declared `excluded`: + no backup tier, no export, no removal touches it. Without that line the whole `media` folder, photos included, would + have gone into every tier and off-site — measured with the controller's own code. Transmission's `watch` folder is + now declared too; before, it went off-site by mistake. +5. **Sonarr/Radarr sign-in default:** seeded. A new install starts with „Authentication Required: Enabled". An existing + install keeps the household's choice. Until the household makes its first admin, other apps on the box can still read + the API key — filed as R-1040. +6. **Claims in the brief that turned out wrong:** + - „a write outside the download folder fails" — the write succeeds, but only inside the container; nothing reaches + the drive (B-3). + - „`/media/shows` in Sonarr and Jellyfin" — also in Plex, and in Jellyfin's prerequisites too. + - Part A: the hub's `/apps` page gives a COUNT per app, not the boxes. The boxes were read one by one; the hub's + „2" = Tester 1 (the rig) + an older report from demo-felhom's sizing bench. + - The first report's top line had been pushed down by my own final edit; restored at the top. + +## What changed + +- **app-catalog `bc54c01`** — sonarr, radarr (one `media` bind, `excluded`; first-start seed; first steps hu+en); + qbittorrent, transmission (only their folder at the same path; `/downloads` kept; default folders moved at start; + watch declared); plex, jellyfin (`/media/tv`); onboarding 2.2 rows; copy freeze (only these lines). CHANGELOG + + REPORT there. +- **felhom.eu** — `09` §3 decisions 242 (D9 = A with a limit) and 243 (the N100 waits); `15` §2 ruled table; evidence. + +## Gates and measurements + +- `catalog_gates.py --fast` for the six apps: all OK (the copy gate first caught a Hungarian suffix error, „filmra" → + „filmre", fixed). +- `check-volume-persistence.py` on the bench: sonarr, radarr, qbittorrent, transmission, jellyfin CLEAN; plex + UNDETERMINED (`/transcode` empty after the exercise; plex's folders did not change in this task). +- Downloads: public domain only, archive.org; removed after the read-back (Radarr deleted the film with its files, + qBittorrent deleted the torrent with its data); both apps removed WITHOUT „delete data". qBittorrent stalled on + archive.org's last piece again; a throwaway Transmission on the bench gave it as a local peer (R-1039). + +## Register + +Rows: before 156 → after 154. Opened 2 (R-1039, R-1040). Closed 4 (R-1014, R-1015, R-1016, R-1030). Narrowed 1 +(R-1019: the watch half fixed; the Tier-2 mirror of optional downloads stays open). STATUS: 52 lines; D9 removed (ruled). + +## Teardown + +Machine: demo-hp 9201 — Radarr and qBittorrent removed, their volumes gone, the household folders kept (empty +`movies`, `torrents/.temp`). Bench 9401 — throwaway containers, folders and the catalog copy removed; the bench is left +RUNNING (it was stopped before this task). Host: nothing changed. Hub: read only. Tester 1 and the N100: untouched. diff --git a/STATUS.md b/STATUS.md index 7f1e6f82..c16246a2 100644 --- a/STATUS.md +++ b/STATUS.md @@ -6,6 +6,7 @@ `documentation/backlog/STATUS-HISTORY.md`. Reports: `REPORT-immich-rollout-2026-10-14.md`, `REPORT-seo-felho-tarhely-2026-10-14.md`, `REPORT-immich-repair-2026-10-14.md`, `REPORT-power-cut-fixes-2026-10-14.md`, `REPORT-night-power-2026-10-13.md`, `REPORT-poweroff.md`. ## What works +- Media apps work together on one drive: a download reaches Sonarr/Radarr as one file, no copy; the download apps see only their own folder (live catalog, proven on demo-hp). - A power cut in the middle of a backup no longer blocks later backups: the box removes the half-written backup at start-up (proven with a real cut on demo-hp, on all three boxes now). - After a power cut, Immich repairs the photos the cut left broken, by itself, and the app page lists any lost photo (proven with a real cut on demo-hp: 41 of 41 repaired; on all three boxes now). An Immich update cannot switch it off quietly: the catalog refuses an untested Immich version. - A household can switch the machine off; the hub holds alarms for 14 days and clears the state by itself (N100). @@ -37,7 +38,6 @@ | 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 - **Clear demo-hp's own floor** (hub → Demo HP → floor: empty → Save floor). It still reads 0.311.0. If not: the next global raise passes demo-hp by. diff --git a/documentation/audits/media-all-drives-2026-10-14/tools/deploy.py b/documentation/audits/media-all-drives-2026-10-14/tools/deploy.py index 9abca0c1..4e0761fd 100644 --- a/documentation/audits/media-all-drives-2026-10-14/tools/deploy.py +++ b/documentation/audits/media-all-drives-2026-10-14/tools/deploy.py @@ -15,8 +15,8 @@ import json, os, secrets, sys, time sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") import box_walk as w -HDD = "/mnt/felhom-drives/media_a" -SEC = os.path.expanduser("~/.felhom-tester1/secrets.json") +HDD = os.environ.get("HDD", "/mnt/felhom-drives/media_a") +SEC = os.path.join(os.environ.get("SC", os.path.expanduser("~/.felhom-tester1")), "secrets.json") def load(): diff --git a/documentation/audits/media-folder-fix-2026-10-14/C-live-cleanup-9201.txt b/documentation/audits/media-folder-fix-2026-10-14/C-live-cleanup-9201.txt new file mode 100644 index 00000000..b7413191 --- /dev/null +++ b/documentation/audits/media-folder-fix-2026-10-14/C-live-cleanup-9201.txt @@ -0,0 +1,13 @@ +radarr: delete movie 1 with files -> 200 +qbittorrent: torrents deleted with their data +left in movies: +left in torrents: .temp + +radarr stop -> 200 +radarr remove WITHOUT delete data -> 200 {'ok': True, 'data': {'removed': 'radarr', 'volumes_removed': ['radarr_radarr_config'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'userdata_kept': ['/mnt/felhom-drives/hdd_1/userdata/media ( +qbittorrent stop -> 200 +qbittorrent remove WITHOUT delete data -> 200 {'ok': True, 'data': {'removed': 'qbittorrent', 'volumes_removed': ['qbittorrent_qbittorrent_config'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'userdata_kept': ['/mnt/felhom-drives/hdd_1/u +0 +/opt/docker/stacks/qbittorrent +/opt/docker/stacks/radarr +audiobooks books comics grimmory metube movies music nzbget photos podcasts torrents transmission tv usenet diff --git a/documentation/audits/media-folder-fix-2026-10-14/C-live-deploy-9201.txt b/documentation/audits/media-folder-fix-2026-10-14/C-live-deploy-9201.txt new file mode 100644 index 00000000..eb97d1ea --- /dev/null +++ b/documentation/audits/media-folder-fix-2026-10-14/C-live-deploy-9201.txt @@ -0,0 +1,22 @@ +21:41:40 qbittorrent: POST deploy {"DOMAIN": "enkisfelhom.hu", "SUBDOMAIN": "torrent", "HDD_PATH": "/mnt/felhom-drives/hdd_1", "QBT_ADMIN_PASSWORD": ""} -> 202 {"ok": true, "message": "The install has started – the card shows how it is going"} +21:41:56 qbittorrent: deployed, state=running, pinned={'qbittorrent': 'qbittorrentofficial/qbittorrent-nox:5.2.4-1'} +running /var/lib/docker/volumes/qbittorrent_qbittorrent_config/_data->/config:true /mnt/felhom-drives/hdd_1/userdata/media/torrents->/downloads:true /mnt/felhom-drives/hdd_1/userdata/media/torrents->/mnt/felhom-drives/hdd_1/userdata/media/torrents:true +2026/10/14 19:41:40 router.go:449: [INFO] [api] Deploy requested for stack: qbittorrent +2026/10/14 19:41:40 deploy.go:526: [INFO] [stacks] Deploying stack qbittorrent with 4 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH, QBT_ADMIN_PASSWORD] +2026/10/14 19:41:40 manager.go:1773: [INFO] [stacks] Deploying stack qbittorrent — checking 1 images... +2026/10/14 19:41:41 notifier.go:353: [INFO] Event pushed: app_deploy_started (info) [+household(en)] — Alkalmazás telepítése elindult: qBittorrent +2026/10/14 19:41:46 deploy.go:616: [INFO] [stacks] Stack qbittorrent deployed successfully (took 5.3s) +2026/10/14 19:41:46 notifier.go:353: [INFO] Event pushed: app_deployed (info) [+household(en)] — Alkalmazás telepítve: qBittorrent +2026/10/14 19:41:51 after_install.go:184: [INFO] [stacks] after_install qbittorrent: qbittorrent [/bin/sh -c curl -s -c /tmp/fc -o /dev/null -X POST -d "username=admin&password=${QBT_ADMIN_PASSWORD}" -H 'Referer: http://127.0.0.1:8080' http://127.0.0.1:8080/api/v2/auth/login && curl -s -b /tmp/fc -o /dev/null -X POST --data-urlencode 'json={"upnp":false,"web_ui_domain_list":"${SUBDOMAIN}.${DOMAIN};qbittorrent;localhost;127.0.0.1"}' -H 'Referer: http://127.0.0.1:8080' http://127.0.0.1:8080/api/v2/app/setPreferences && curl -s -b /tmp/fc http://127.0.0.1:8080/api/v2/app/preferences | grep -q '"upnp":false' && curl -s -o /dev/null -w '%{http_code}' -H 'Host: qbittorrent:8080' http://127.0.0.1:8080/ | grep -qE '^(200|401|403)$' && echo FELHOM_AFTER_INSTALL_OK +2026/10/14 19:41:51 install_hold.go:135: [INFO] [stacks] qbittorrent: install hold OPENED by after_install — the app is reached as without a hold + +21:42:00 radarr: POST deploy {"DOMAIN": "enkisfelhom.hu", "SUBDOMAIN": "radarr", "HDD_PATH": "/mnt/felhom-drives/hdd_1"} -> 202 {"ok": true, "message": "The install has started – the card shows how it is going"} +21:42:30 radarr: deployed, state=running, pinned={'radarr': 'lscr.io/linuxserver/radarr:6.4.4'} +running /var/lib/docker/volumes/radarr_radarr_config/_data->/config:true /mnt/felhom-drives/hdd_1/userdata/media->/mnt/felhom-drives/hdd_1/userdata/media:true +2026/10/14 19:41:59 router.go:449: [INFO] [api] Deploy requested for stack: radarr +2026/10/14 19:42:00 deploy.go:526: [INFO] [stacks] Deploying stack radarr with 3 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH] +2026/10/14 19:42:00 manager.go:1773: [INFO] [stacks] Deploying stack radarr — checking 1 images... +2026/10/14 19:42:00 notifier.go:353: [INFO] Event pushed: app_deploy_started (info) [+household(en)] — Alkalmazás telepítése elindult: Radarr +2026/10/14 19:42:10 deploy.go:616: [INFO] [stacks] Stack radarr deployed successfully (took 10.0s) +2026/10/14 19:42:10 notifier.go:353: [INFO] Event pushed: app_deployed (info) [+household(en)] — Alkalmazás telepítve: Radarr + diff --git a/documentation/audits/media-folder-fix-2026-10-14/C-live-readback-9201.txt b/documentation/audits/media-folder-fix-2026-10-14/C-live-readback-9201.txt new file mode 100644 index 00000000..21666b7e --- /dev/null +++ b/documentation/audits/media-folder-fix-2026-10-14/C-live-readback-9201.txt @@ -0,0 +1,27 @@ +== Radarr: what the first steps say +drwxrwsr-x 2 root users 4096 Jul 21 19:08 /mnt/felhom-drives/hdd_1/userdata/media/movies +drwxrwsr-x 2 abc users 4096 Oct 13 21:26 /mnt/felhom-drives/hdd_1/userdata/media/torrents +audiobooks books comics grimmory metube movies music nzbget photos podcasts torrents transmission tv usenet + root folder /mnt/felhom-drives/hdd_1/userdata/media/movies -> 201 + download client qBittorrent (its own connection test) -> 201 + movie added: Night of the Living Dead (1968) path=/mnt/felhom-drives/hdd_1/userdata/media/movies/Night of the Living Dead (1968) + release push -> 200 approved=True rejections=[] + grabbed by qBittorrent + +65s queue: downloading downloading left=594823937 output=None + +125s queue: downloading downloading left=300174081 output=None + +185s queue: downloading downloading left=300174081 output=None + +245s queue: downloading downloading left=173673216 output=None + +305s queue: downloading downloading left=72043264 output=None + +365s queue: downloading downloading left=72043264 output=None + +425s queue: downloading downloading left=35585 output=None + +485s queue: warning downloading left=35585 output=None + +545s queue: warning downloading left=35585 output=None + +605s queue: warning downloading left=35585 output=None + +665s queue: warning downloading left=35585 output=None + +725s queue: warning downloading left=35585 output=None + +735s history downloadFolderImported: {"downloadClientName": "qBittorrent", "droppedPath": "/mnt/felhom-drives/hdd_1/userdata/media/torrents/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4", "importedPath": "/mnt/felhom-drives/hdd_1/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4"} + download: links=2 inode=9568271 library: links=2 inode=9568271 + RESULT: HARDLINK — one file, two names +# NOTE: qBittorrent stalled on the LAST piece (archive.org's web seed; the same fault as on Tester 1 that afternoon). At +# 21:53 a throwaway Transmission on the bench (192.168.0.118, LXC 9401) fetched the same public-domain torrent and was +# given to qBittorrent as a peer (addPeers); qBittorrent completed at 21:54:43 and Radarr imported at +735 s. diff --git a/documentation/audits/media-folder-fix-2026-10-14/tools/live_readback.py b/documentation/audits/media-folder-fix-2026-10-14/tools/live_readback.py new file mode 100644 index 00000000..04bcaf37 --- /dev/null +++ b/documentation/audits/media-folder-fix-2026-10-14/tools/live_readback.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""live_readback.py — Part B6: the one-drive chain on a box with the LIVE templates, run INSIDE the guest. + +Does what the new first steps tell the household to do — Radarr: root folder `.../media/movies`; one qBittorrent +entry (host qbittorrent, port 8080, admin, the install's password, category `movies`) — then hands ONE public-domain +archive.org film to Radarr by its release push and waits for the import. Proof: download path, library path, link +count, inode, Radarr's history entry. Keys and passwords are read at runtime and never printed. + + run_in_guest-style: python3 live_readback.py +""" +import datetime, json, os, re, subprocess, sys, time, urllib.error, urllib.request + +HDD = sys.argv[1] +LIB = f"{HDD}/userdata/media/movies" +ITEM = "night-of-the-living-dead-1968_202508" # CC Public Domain Mark, 597 MB +SECRETS = [] + + +def red(s): + s = str(s) + for x in SECRETS: + s = s.replace(x, "") if x else s + return s + + +def say(*a): + print(red(" ".join(str(x) for x in a)), flush=True) + + +def sh(c): + return subprocess.run(c, shell=True, capture_output=True, text=True).stdout.strip() + + +def ip(c): + return sh(f"docker inspect {c} -f '{{{{range .NetworkSettings.Networks}}}}{{{{.IPAddress}}}}{{{{end}}}}'") + + +key = re.search(r"([^<]+)", open("/var/lib/docker/volumes/radarr_radarr_config/_data/config.xml").read()).group(1) +pw = sh("docker exec qbittorrent printenv QBT_ADMIN_PASSWORD") +SECRETS += [key, pw] +RA = f"http://{ip('radarr')}:7878/api/v3" +H = {"X-Api-Key": key, "Host": "radarr", "Content-Type": "application/json"} + + +def ra(m, p, body=None): + r = urllib.request.Request(RA + p, data=json.dumps(body).encode() if body is not None else None, headers=H, method=m) + try: + with urllib.request.urlopen(r, timeout=120) as x: + t = x.read(); return x.status, (json.loads(t) if t else None) + except urllib.error.HTTPError as e: + b = e.read().decode()[:400]; say(f" !! {m} {p} -> {e.code} {b}"); return e.code, None + + +say("== Radarr: what the first steps say") +print(sh(f"docker exec radarr sh -c 'ls -ld {LIB} {HDD}/userdata/media/torrents; ls {HDD}/userdata/media | tr \"\\n\" \" \"'")) +if not any(r["path"].rstrip("/") == LIB for r in ra("GET", "/rootfolder")[1]): + c, r = ra("POST", "/rootfolder", {"path": LIB}); say(f" root folder {LIB} -> {c}") +sch = {s["implementation"]: s for s in ra("GET", "/downloadclient/schema")[1]} +if not ra("GET", "/downloadclient")[1]: + b = json.loads(json.dumps(sch["QBittorrent"])) + vals = {"host": "qbittorrent", "port": 8080, "username": "admin", "password": pw, "movieCategory": "movies"} + for f in b["fields"]: + if f["name"] in vals: + f["value"] = vals[f["name"]] + b.update({"name": "qBittorrent", "enable": True}) + c, r = ra("POST", "/downloadclient", b); say(f" download client qBittorrent (its own connection test) -> {c}") +c, lk = ra("GET", "/movie/lookup/tmdb?tmdbId=10331") +qp = ra("GET", "/qualityprofile")[1][0] +c, m = ra("POST", "/movie", dict(lk, qualityProfileId=qp["id"], rootFolderPath=LIB, monitored=True, tags=[], + minimumAvailability="released", addOptions={"searchForMovie": False})) +say(f" movie added: {m['title']} ({m['year']}) path={m['path']}") +rel = {"title": "Night.of.the.Living.Dead.1968.480p.WEB-DL.x264-ARCHIVEORG", "protocol": "torrent", "size": 596645320, + "downloadUrl": f"https://archive.org/download/{ITEM}/{ITEM}_archive.torrent", "indexer": "archive.org (release push)", + "publishDate": datetime.datetime.now(datetime.timezone.utc).isoformat()} +t0 = time.time() +c, d = ra("POST", "/release/push", rel) +dd = d[0] if isinstance(d, list) else (d or {}) +say(f" release push -> {c} approved={dd.get('approved')} rejections={dd.get('rejections')}") +imp = None +for i in range(150): + time.sleep(10) + for e in ra("GET", f"/history/movie?movieId={m['id']}")[1] or []: + if e["eventType"] == "grabbed" and i == 0: + say(f" grabbed by {e['data'].get('downloadClientName')}") + if e["eventType"] == "downloadFolderImported": + imp = e + if imp: + break + if i % 6 == 5: + for r in (ra("GET", f"/queue?movieIds={m['id']}")[1] or {}).get("records", []): + say(f" +{int(time.time()-t0)}s queue: {r.get('status')} {r.get('trackedDownloadState')} left={r.get('sizeleft')} output={r.get('outputPath')}") +if not imp: + say(f" RESULT: not imported within {int(time.time()-t0)}s"); sys.exit(1) +e = imp["data"] +say(f" +{int(time.time()-t0)}s history downloadFolderImported: {json.dumps({k: e.get(k) for k in ('downloadClientName', 'droppedPath', 'importedPath')})}") +a, b = os.stat(e["droppedPath"]), os.stat(e["importedPath"]) +say(f" download: links={a.st_nlink} inode={a.st_ino} library: links={b.st_nlink} inode={b.st_ino}") +say(" RESULT:", "HARDLINK — one file, two names" if (a.st_ino, a.st_dev) == (b.st_ino, b.st_dev) else "COPY") diff --git a/documentation/backlog/CLOSED-ITEMS.md b/documentation/backlog/CLOSED-ITEMS.md index 28cd193b..a13c02ab 100644 --- a/documentation/backlog/CLOSED-ITEMS.md +++ b/documentation/backlog/CLOSED-ITEMS.md @@ -26,6 +26,15 @@ --- +## 2026-10-14 — the media folders meet in the live catalog + +| Row | What | Closed | Evidence | +|---|---|---|---| +| **R-1014** | **The media chain could not work: the download folders did not meet, and the hardlink comment was false** (P2) | CLOSED 2026-10-14 — FIXED in the live catalog (decision 242, D9 = A with a limit): Sonarr/Radarr bind `media` at the same path, the clients only their own folder at the same path; the false comment replaced by the measured reason. Live read-back on demo-hp 9201: one public-domain film qBittorrent → Radarr imported as a HARDLINK (links 2, one inode). Installed clients keep their old torrents (bench). | app-catalog `bc54c01`; `audits/media-folder-fix-2026-10-14/``C-live-readback-9201.txt`, `B2-B3-*.txt` | +| **R-1015** | **Three apps told the household to use `/media/shows`** (P3) | CLOSED 2026-10-14 — FIXED: Sonarr, Plex and Jellyfin first steps (hu + en) name `media/tv`; the copy freeze re-captured for those lines only. | app-catalog `bc54c01` | +| **R-1016** | **qBittorrent's onboarding record named the wrong download folder** (P4) | CLOSED 2026-10-14 — FIXED: `onboarding/qbittorrent.md` and `transmission.md` row 2.2 name the real folders, with the bench evidence. | app-catalog `bc54c01` | +| **R-1030** | **Sonarr's and Radarr's first admin must default to „Enabled”** (P2) | CLOSED 2026-10-14 — FIXED for new installs: the first start seeds `AuthenticationRequired=Enabled` into `config.xml`; the first-admin form then reads „enabled”; an existing `config.xml` is never touched (bench). The window BEFORE the household makes its admin stays open to other containers — filed as R-1040. | app-catalog `bc54c01`; `audits/media-folder-fix-2026-10-14/``B2-sonarr-radarr.txt` | + ## 2026-10-14 — the Immich repair on every box; the shared checkout cleaned | Row | What | Closed | Evidence | diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index aaaf1812..ade59dc4 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -124,17 +124,14 @@ 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 — 19 rows (P2 3, P3 11, P4 5) +## Apps & catalog — 15 rows (P2 1, P3 10, P4 4) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| -| **R-1014** | Apps & catalog | P2 | **The media chain cannot work: the download clients save where Sonarr and Radarr never look, and the templates' hardlink promise is false.** Live catalog `main`: qBittorrent saves in `${USERDATA_PATH}/media/torrents`, Transmission in `media/transmission`; Sonarr and Radarr see `/downloads` = `${USERDATA_PATH}/downloads` and `/media/tv` or `/media/movies` — a download never reaches a library. The comment „SIBLING of media — same FS → hardlink/atomic move” (sonarr/radarr `docker-compose.yml` line 12) is FALSE: two bind mounts are two mount points, and `link(2)` answers EXDEV even on one filesystem. MEASURED 2026-10-14 on Tester 1 (S1): today's two-bind shape → `ln: … Cross-device link`, the import is a copy (link count 1, two inodes); one bind per drive at the same path → link count 2, one inode. S2: with that layout, one public-domain film went Radarr → Transmission → drive B → import as a hardlink. One fix closes both (the comment goes with the binds). Evidence `audits/media-all-drives-2026-10-14/``S1-hardlink.txt`, `S2-*.txt`; design `architecture/15-media-library.md`. | **READY** — filed 2026-10-14 | — | The live-fix task (`TASK-media-folder-fix-and-n100-2026-10-14.md`) moves the six templates to the layout of `15` §2 | CC | -| **R-1030** | Apps & catalog | P2 | **Sonarr's and Radarr's first admin must default to „Enabled”, not „Disabled for Local Addresses”.** MEASURED 2026-10-14 on Tester 1 (`audits/media-all-drives-2026-10-14/``G-auth/`) with the operator's choice „Disabled for Local Addresses”: an outside visitor IS asked to sign in — the tunnel delivers the real public address in `X-Forwarded-For` (qBittorrent logged it) and Sonarr/Radarr then send `/` and `/initialize.json` to `/login`. But a request WITHOUT that header — any device on the home network, and any other container on the box's `traefik-public` network — gets the page and `/initialize.json` WITH the API key, no sign-in (200, `apiKey` present). One compromised catalog app can then drive Sonarr and Radarr. Today the box's setup gate also blocks outsiders until setup is marked done. | **READY** — filed 2026-10-14 | — | Seed `AuthenticationRequired=Enabled` (with `AuthenticationMethod=Forms`) in the templates' first-start config, the same way qBittorrent seeds its password; say in the first steps why | CC | | **R-981** | Apps & catalog | P3 | **The homepage app gets the Docker socket (that is root in the guest) and has no login, no setup gate and no family gate.** Fix: remove the socket mount, or put homepage behind the family gate. Security review 2026-10-12 (`audits/security-review-2026-10-12/`); detail held off-git for the operator (`/mnt/5_hdd/felhom.eu/evidence/security-review-2026-10-12-PRIVATE/`). | OPEN | — | Change the catalog template; prove on 9202 | CC | | **R-971** | Apps & catalog | P2 | **A change pushed to the catalog's `main` reaches the installed apps on every box within about 15 minutes, and nothing on the box refuses dangerous container settings (privileged mode, the Docker socket, host mounts, host namespaces); so whoever can push to the catalog repository can take every guest.** Design-shaped: installed apps take catalog fixes on purpose. Decision for the operator in `STATUS.md`. Fix options: a privilege allow-list check on the box before any compose is written (and in catalog CI), and later signed catalog releases. Security review 2026-10-12 (`audits/security-review-2026-10-12/`); detail held off-git for the operator (`/mnt/5_hdd/felhom.eu/evidence/security-review-2026-10-12-PRIVATE/`). | WAITING-ON-OPERATOR — confirmed by a second reader | operator decision | Decide; then build the box-side check | operator | | **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-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 — 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). | **OPEN** — filed 2026-10-14 | R-1023 | Build with R-1023 | CC | @@ -146,7 +143,6 @@ stopping line that lies. | **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-1016** | Apps & catalog | P4 | **qBittorrent's onboarding record names the wrong download folder.** `onboarding/qbittorrent.md` row 2.2 says `${USERDATA_PATH}/media/qbittorrent`; the template binds `media/torrents` (and the skeleton makes `media/torrents`). Record only — the app is right. Found 2026-10-14. | **READY** — filed 2026-10-14 | — | Correct the row with the live-fix task's catalog commit | CC | ## App updates — 4 rows (P3 4) @@ -181,7 +177,7 @@ stopping line that lies. | **R-548** | Backup & restore | P3 | **[P3-LOW] The whole-guest backup’s LOCAL tier cannot fit on a small-system-disk box, and will retry on that tier for ever.** MEASURED 2026-09-17 (chaos night) on `tester-1-022354`: a whole-guest backup wrote a **~29 GB source** (`mp0` = `local-lvm:vm-9201-disk-1`, 70 G provisioned, 40.58 % used, `backup=1`) into `pve-root`, which on a 32 GB system disk is **14 GB total with ~4.9 GB free**. Two samples thirty seconds apart showed the archive growing ~497 MB while free space fell ~475 MB — **~16 MB/s, i.e. under four minutes to a full `/`** on the nested PVE. **The product’s behaviour is correct and legible throughout:** it failed the tier and said which one — `whole_guest_backup_failed` (error, operator-only): „Whole-guest backup FAILED on the local tier — retrying with backoff (next attempt in 15m0s)” — its status surface agreed (`target_id:"local"`, `success:false`, `size_bytes:0`), and the **off-site tier then ran from the same snapshot and succeeded in ~8½ minutes, encrypted to ep0, consuming no local disk at all**. So the data still left the house. What is filed is the loop: on a box shaped like this the local tier can **never** succeed, and it keeps retrying on a backoff for ever, burning I/O and risking `/` each time. **Honest caveat:** the 32 GB system disk is this drill’s fixture choice, so the row is conditional on disk size — but nothing in the product checks whether the local target could ever hold the source before trying. **Fix shape:** compare the source size against the target’s free space before starting the local tier and skip it with a clear reason, rather than discovering it at ~16 MB/s. Evidence: `audits/evidence-chaos-night-2026-09-17/round-6.txt`. **-- 2026-09-30, the class on a real box: demo-hp's whole-guest LOCAL tier was refused by the space preflight (R-685) 10 times since 2026-09-27** („local has 12.1 GiB free; the last archive of guest 9201 was 8.9 GiB, so a new one needs about 12.1 GiB") — its newest local archive was 2026-09-29 04:42, 30 h old at the day's read (the weekly PBS tier, last 2026-09-24, is its whole copy meanwhile). The refusal is correct and named; what is missing is that nothing gives the box the room back (the host's `local` holds the 8.9 GiB archive of the only guest plus templates on a 39 GB root). `audits/pg-last-six-2026-09-30/C/`. | **READY — rank P3-LOW; owner: CC** | — | — | CC | | **R-921** | Backup & restore | P3 | **When the off-site tier answers BUSY, the household still gets a short stop of every app — with no copy.** MEASURED 2026-10-08 night on demo-hp (read-back 2026-10-09, `audits/dooplex-survival-2026-10-09/partE/R-518.txt`): local tier 20:20–20:25Z (own stop, < 2 min), then at 20:26:14Z the agent refused the off-site request (`backup refused — a heavy operation is already in flight`, `busy=backup:local` — the night OS step ran 20:27–20:29Z), yet the controller's metrics show all 15 app containers down at 20:26:19Z and back at 20:27:19Z; the retry ran the off-site tier at 20:34Z with its own short stop. So a two-tier night costs THREE stops, one of them for nothing. This was R-518's noted "unmeasured" risk. Fix shape: ask the agent whether it is free (or reserve the slot) BEFORE quiescing, and resume at once on BUSY. | **NARROWED 2026-10-09 — built; RELEASED 2026-10-10 (controller 0.305.0 on demo-hp, demo-felhom, Tester 1):** before stopping apps the controller asks the agent which tiers have a job in flight (`GET /backup/status`); another tier's job in flight → no stop, the tier stays due (red-proved: 3 apps stopped for a refused tier before); a refusal after the stop still resumes the apps at once (pinned). Also fixes a stop after a long upload or a restart mid-upload. **LEFT: the measured case itself** — the agent's host-wide busy lock (the night OS step, restore test, fstrim) is served on NO endpoint, so the controller cannot see it before stopping; needs one agent field (e.g. `busy` on `GET /backup/status`) + a controller pre-check behind a feature probe. `07` §6.4. | an agent change | Build the agent `busy` field + its controller check; read back a two-tier night | CC | | **R-933** | Backup & restore | P3 | **Mirror the images of every catalog app we support into our own registry, before scale.** Operator, 2026-10-12 (S8, `09` §3 decision 203): *„Will need to mirror the apps we support later, just to be sure."* Today a restore re-pulls each image from its maker; a version the maker deleted cannot start (the accepted limit, `07` §6.6, R-698 closed). Size it first: the supported catalog's images (every ladder version a box can hold) × their size; then where the copy lives (the DooPlex registry is Tier 2, precious — the copy must not make DooPlex a single point of failure for every household's restore) and how a restore falls back to it. | **OPEN** — NEW 2026-10-12 | the size measurement | Measure the total size of the supported catalog's images; then a one-page design (where, how a restore falls back, cost) | CC (measure + design), operator (the go) | -| **R-1019** | Backup & restore | P3 | **The live download-client templates put downloads into two backup tiers nobody chose.** MEASURED 2026-10-14 with the controller's own classifier over the live templates (`audits/media-all-drives-2026-10-14/``A-drives/A6-3-capture-sets.txt`): Transmission's `${USERDATA_PATH}/media/transmission/watch` bind is not listed in its `backup:` block, so it is classed MANDATORY and goes into the OFF-SITE copy (a writable unlisted bind defaults to mandatory); and qBittorrent's `media/torrents` and Transmission's `media/transmission` are `optional`, so on a box with a second drive every download is mirrored there nightly (Tier 2 = mandatory + optional) — it can fill the backup drive with films. | **READY** — filed 2026-10-14 | — | Declare the media binds `excluded` (the watch folder too) in the live-fix task; `15` §7 states the rule for the per-drive bind | CC | +| **R-1019** | Backup & restore | P3 | **The live download-client templates put downloads into two backup tiers nobody chose.** MEASURED 2026-10-14 with the controller's own classifier over the live templates (`audits/media-all-drives-2026-10-14/``A-drives/A6-3-capture-sets.txt`): Transmission's `${USERDATA_PATH}/media/transmission/watch` bind is not listed in its `backup:` block, so it is classed MANDATORY and goes into the OFF-SITE copy (a writable unlisted bind defaults to mandatory); and qBittorrent's `media/torrents` and Transmission's `media/transmission` are `optional`, so on a box with a second drive every download is mirrored there nightly (Tier 2 = mandatory + optional) — it can fill the backup drive with films. | **NARROWED** — filed 2026-10-14; the `watch` half FIXED 2026-10-14 (declared optional, app-catalog `bc54c01`); open: the `optional` download folders still get a nightly Tier-2 mirror on a box with a second drive | — | Declare the media binds `excluded` (the watch folder too) in the live-fix task; `15` §7 states the rule for the per-drive bind | CC | | **R-91** | Backup & restore | P4 | Old 13 GB datastore copy at `/srv/pbs-felhom` on ep0's root disk **Checked from source 2026-10-05 (burn-down round 2):** Gate is long past (row waits on demo-felhom's first post-migration PBS backup, migration 2026-07-27). Last positive record of the copy: audits/CAMPAIGN-9-restore-proof-2026-07-28.md:759 'ep0 : /srv/pbs-felhom rollback copy intact (13G)'; CONTEXT.md:3666 still says it is 13 G of dead weight awaiting R-91. No later record of deletion found (grep srv/pbs-felhom across felhom.eu). Deleting is on ep0 (protected) and needs an operator word. | WATCHING | demo-felhom's first **post-migration** PBS backup | Delete once it lands; fix `CONTEXT.md:1018` same commit | CC | | **R-164** | Backup & restore | P4 | **C2's chain: the DB volume tar cannot be dropped until a SOUND dump predicate exists.** The unit carries both a volume tar and a SQL dump; the restore uses **both** — the dump is authoritative and replayed *after* the tar so it WINS (F17), with only the DB service up (R-47) — `internal/backup/restore_unit.go:262-266`. Dropping the DB container's tar would halve DB-app units **and** close the R-127(b) initdb-skip password trap (restored PGDATA ⇒ `POSTGRES_PASSWORD` ignored). | **BLOCKED** — on the predicate | a dump-validity predicate that is not `accounts has rows` | **The obvious gate is DEAD, measured:** `ValidateDump` warns when the `accounts` table is empty, and that warning was **correct** — the live DB genuinely had 0 accounts, and seeding one stopped the warning and put the row in the dump. But **a fresh appliance legitimately has zero accounts**, so promoting that predicate to a gate would **block every new customer's first backup**. Order: (1) a sound predicate — dump vs **live** per-table counts, not an absolute expectation; (2) warn→gate; (3) tar-drop. **Until (1), the tar is load-bearing** — not because dumps are bad, but because nothing can yet prove one is good. Pairs with **R-127** | CC | | **R-213** | Backup & restore | P4 | **Putting files back in place — the half the recovery screen deliberately does not do.** The screen (R-193, controller v0.200.0) unlocks the repository and LISTS what is in it; restoring is per-app and lives in the backups area, and the operator ruled the two separate on 2026-08-05: *a screen that unlocks and then offers to overwrite is two decisions wearing one button*. What is missing is the step after the listing — a customer who can now SEE their files still has to work out, per app, which restore to choose. **The operator named its requirement: a live-versus-backup comparison** — the customer must be able to see what would change before anything is overwritten | **DEFERRED** — operator ruling 2026-10-12 (S18, `09` §3 decision 213): designed after the first customers. Stays P4; re-dated: look again when the first paying household has run 30 days. **OPEN — not started, deliberately** | the comparison design (nothing exists for it yet) | After the first customers | Operator + CC | @@ -206,7 +202,7 @@ stopping line that lies. | **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 | -## Security & access — 17 rows (P1 1, P2 4, P3 8, P4 4) +## Security & access — 18 rows (P1 1, P2 4, P3 8, P4 5) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| @@ -227,6 +223,7 @@ stopping line that lies. | **R-925** | Security & access | P1 | **`manifests/felhom.secret.yaml` carries REAL secret values, and the Gitea repo that holds it is readable by anyone on the internet with no login.** FOUND 2026-10-09 by a background security review of the legal-pages commit, which flagged the served `` comments; chasing what those comments pointed at found this instead. **MEASURED, in this order:** (1) `scripts/manifest_bearer_gate.py` itself prints `manifests/felhom.secret.yaml:39 KNOWN-BACKLOG committed secret 65cee3c4…7a86`; (2) the file holds **live-shaped values, not placeholders** — `SECRET_KEY` (69 chars), `SUPERUSER_EMAIL`/`SUPERUSER_PASSWORD` (18), Umami `APP_SECRET` (66) and `POSTGRES_PASSWORD` (34), and a `username`/`password` pair (18); values were never printed, only measured by length; (3) `gitea.dooplex.hu` resolves to **37.191.56.193**, the same public address as the website; (4) an **anonymous** `curl` returns HTTP 200 and 1686 bytes for that exact file, and 200 for `hub/internal/store/store.go` and `manifests/hub.yaml`; (5) **the off-network control**, which is what makes (4) mean anything — fetched from outside the operator's network entirely: a real path returns the file's first line, a nonsense path returns 404, so the 200 is genuine anonymous read from the internet and not a LAN-only ACL. **This contradicts the project's own written model**: `documentation/runbooks/secrets.md:3-4` says *“Secret values are never committed to git. The manifests in `manifests/` carry only placeholders + comments that point here.”* That promise is false today, which is the P1 wording in this register's own scale — see the ranking note below. The same runbook already states the rule that governs the fix: *“The git-history copy stays alive until the value is ROTATED — de-git alone kills nothing.”* **Known neighbours, none of which cover this:** R-887 records that an outside crawler walks the public Gitea pages and leaves *“Gitea's exposure to the crawler”* as the operator's undone item; R-580 still owes a Gitea admin token rotation. Neither says a secret file is world-readable. **Why P2 and not P1, stated so the operator can overrule it:** the exposed credentials guard **analytics and an undeployed healthchecks instance, not household data** — measured: `umami-db` is a **ClusterIP** service on 5432 with no external IP, so the Postgres password is not reachable from the internet; the realistic harm is session forgery against the public `stats.felhom.eu` via `APP_SECRET`, and reuse of those passwords anywhere else. No customer box, hub token or escrow key is in this file. **CC did NOT change anything**: making the repo private could break the public day-0 path (the installer is fetched from a public tag, R-110), and rotation plus repository visibility are operator decisions on production infrastructure. **PARTLY FIXED 2026-10-09, and RE-RANKED P2 -> P1 on what it turned out to be.** The exposed value was not an analytics password: it was the **Gitea `admin` account password** (`is_admin: true`; `/api/v1/admin/users` answered 200), published in a repo `gitea.dooplex.hu` serves anonymously to the internet. That is push access to every repo — including the one whose `website/` is git-synced live and whose `scripts/` is published by tag to **every new box installer** (R-110), i.e. a supply-chain path onto customer hardware. My first ranking said P2 because I had only measured the analytics blast radius; the credential test is what corrected it. **DONE (CC, operator-instructed):** (1) `umami-config` `APP_SECRET` + `POSTGRES_PASSWORD` rotated — the password changed **inside Postgres** too (`ALTER USER`), because the env var is only read at first init; verified by a real beacon returning 200 with a bogus-site-id control still returning 400. (2) `healthchecks-config` `SECRET_KEY` + `SUPERUSER_PASSWORD` rotated (nothing consumes them — there is no healthchecks Deployment). (3) `gitea-creds` no longer holds the admin password at all: it holds a **scoped token** (`read:package` + `read:repository`). **Both scopes are load-bearing and the second was found by breaking it** — a package-only token made the hub log `Template fetch: unexpected status 403`, because the template fetcher reads a raw file out of the `felhom-controller` repo. (4) The Gitea **admin password** was changed and `gitea-system/gitea-admin` updated; **verified: new password 200, old published password 401 — the leak is dead.** (5) The file is removed from git and `.gitignore`'s `*secret*` now applies to it; `manifest_bearer_gate.py`'s `KNOWN_BACKLOG` carve-out is gone, red-proofed with a decoy (exit 1 with, 0 without). **AN INCIDENT CAUSED BY THE FIX, recorded because it is the useful part:** the `rollout restart` needed to pick up the new umami secret put umami into **CrashLoopBackOff and took `stats.felhom.eu` down (503)**. Cause was not the rotation — at `memory: 512Mi` the pod runs for months but **cannot restart**, because startup (Prisma + Next.js) peaks over the limit and is OOMKilled (exit 137). Raised to 1Gi, **in the manifest, not just live** (the `.claude/rules/manifests.md` rule: a bare `kubectl set` is reverted by the next sync and the fix silently disappears). Service restored and verified. New values are in a 0600 file on DooPlex (`~/rotated-secrets-2026-10-09.txt`), never echoed; the operator moves them to the password manager and deletes it. **WHAT REMAINS, and it is bigger than what was fixed:** that one password is still the admin password in about **12 other namespaces** — `nextcloud`, `paperless`, `bookstack` (a **database root** password), `tandoor`, `calibre`, `adventurelog`, `gokapi`, `qbittorrent` (x2), `servarr`, `homepage` (x2). Rotating Gitea does not touch them: each is its own login and each is still the string that was published. **Also owed:** a Gitea token for user `kisfenyo` sits in plaintext in the `origin` URL of the local `homelab-manifests` clone (`.git/config`); CC printed it to a session transcript while investigating, so it should be rotated regardless — this is R-580's shape (store the remote without credentials). **Not a finding:** `homelab-manifests` itself is private (404 anonymously) and does not contain the password; ArgoCD's repo credential is a separate token, so the rotation did not touch it. | **NARROWED 2026-10-09 — the felhom.eu half is DONE and verified (Gitea admin password rotated, old one now 401; umami + healthchecks rotated; file de-gitted; gate carve-out removed). What remains is the ~12 reused logins elsewhere in the homelab and the repo-visibility decision; owner: operator** **2026-10-10 (release session), two consequences measured:** (a) DooPlex's own Docker login for `gitea.dooplex.hu` (`~/.docker/config.json`) still holds the OLD admin password — the registry token endpoint answers 401 for it and 200 for the current one — so every image push from DooPlex fails `unauthorized` until it is refreshed; the 0.145.0/0.305.0 pushes used a one-off login in a scratch `DOCKER_CONFIG`, shredded after. (b) The `felhom` ArgoCD app reads OutOfSync for `Secret/gitea-creds`, `Secret/healthchecks-config`, `Secret/umami-config` and `Deployment/umami`: a whole-app sync would push git's (de-gitted) view over the rotated live Secrets — the hub deploy synced ONLY `Deployment/hub` (`audits/release-2026-10-10/hub-0.145.0-deploy.txt`). Refreshing the DooPlex login is a DooPlex change (operator's word). | — | **OPERATOR RULED 2026-10-09: (a) he rotates the remaining services himself — CC's scope stopped at the Felhom boundary; (b) the repo STAYS anonymously readable for now**, because making it private breaks the website git-sync and the installer tag fetch, which both clone with no credentials (R-110). The standing consequence of (b): no secret may ever enter this repo again, which `manifest_bearer_gate.py` now enforces with no exemption; and if (b) is reversed, the 32 `` comments on /adatkezeles must be stripped in the same change. **The exact checklist of the 12 remaining secrets** (namespace / secret / key, re-measured after the rotation, with the two traps that bite — a DB password is not changed by editing the Secret, and a pod that has run for months may not restart) is in `documentation/runbooks/secrets.md`. Operator: **(1)** work that checklist, `bookstack-db/root-password` first because it is a database root password and every one of these hosts answers on the public internet; (`bookstack` first — it is a database root password); **(2)** rotate the `kisfenyo` Gitea token embedded in the local `homelab-manifests` remote URL, and store the remote without credentials; **(3)** decide whether `gitea.dooplex.hu` should answer anonymously at all — remembering the website git-sync clones it with no credentials and the installer is fetched from a public tag, so making it private breaks both unless they get credentials first, and the 32 `` comments on /adatkezeles become a map of it; **(4)** move `~/rotated-secrets-2026-10-09.txt` into the password manager and delete it. If nothing is done: the published password keeps opening a dozen services, even though Gitea itself is now safe | operator | | **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 | ## Box system & updates — 11 rows (P2 1, P3 7, P4 3) @@ -297,7 +294,7 @@ stopping line that lies. | **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 | | **R-1035** | Business & legal | P4 | **Did the „felhő tárhely” page earn anything? Two dated checks.** **Baseline (reported by the operator from Search Console, 2026-10-14, last 3 months):** 1 click, 48 impressions, CTR 2.1 %, average position **73.6**; „felhő tárhely” 19 impressions, „felhőtárhely” 5, „felhő alapú tárhely” 3. CC measured separately that the phrase „felhő tárhely” appeared **0 times** on the home, FAQ and backup pages — Google was showing the home page for those queries by accident. `/sajat-felho-tarhely` + `/en/own-cloud-storage` published **2026-10-14**; full baseline, price sources and the power provenance in `audits/seo-2026-10-14/BASELINE.md`. **What to measure, both times:** impressions and average position for „felhő tárhely” and „saját felhő tárhely”, plus the new page's own impressions, clicks and position. **A null result at +4 weeks means nothing on its own** — a new page on a site at position 73.6 takes weeks to months to move, and „felhő tárhely” alone is held by Microsoft, Google and the big list sites; the page aims at the two longer phrases. That is why there are two checks and not one, and why neither should be read as a verdict on the copy. **There is no Search Console connector in this project, so CC cannot read these numbers** — the operator reads them, CC records and compares. **Blocked until the operator asks for indexing** (Search Console → URL-ellenőrzés → the URL → „Indexelés kérése”): until that happens, or Google finds the page itself, both checks read zero for a reason that is not the page. **This is a slow channel and does not replace talking to people.** | **WATCHING** | operator: request indexing, then supply the Search Console read | 2026-11-11: first read (+4 weeks); 2026-12-09: second (+8 weeks), compared with the first | operator + CC | -## Process & tooling — 26 rows (P3 11, P4 15) +## Process & tooling — 26 rows (P3 10, P4 16) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| @@ -326,6 +323,7 @@ stopping line that lies. | **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/`), and the test's own scratch repos inherit it — its `git commit` fails. Standalone: 30/30 OK; with `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 | +| **R-1039** | Process & tooling | P4 | **qBittorrent stalls on the LAST piece of an archive.org torrent; Transmission does not.** Seen three times on 2026-10-14 (Tester 1 S2 and S3, demo-hp 9201 live read-back): the film reaches 99.994 %, the piece shared with archive.org's side files (`__ia_thumb.jpg`, `_meta.sqlite`, `_meta.xml`) never arrives, qBittorrent's log shows „500 Internal Server Error” from the archive.org web seed, while Transmission finishes the same torrent and curl range requests on the same files answer 206. A household that uses archive.org torrents with qBittorrent hits it too; our tests finish only with a local peer. | **OPEN** — filed 2026-10-14 | — | Note it in qBittorrent's onboarding record; for test films use Transmission, or a public-domain torrent with real peers | CC |