Media all drives, Parts A-C on Tester 1: S1-S3 proven (hardlink with one bind per drive; today's shape copies); R-1014..R-1019 filed
gates / gates (push) Successful in 7m46s
gates / gates (push) Successful in 7m46s
Two test drives enrolled through the dashboard; no test media in any backup tier (A6, three proofs); Tester 1 follows a credential-free local mirror of the drill catalog (R-992 worked around a third way). The live-fix task may start.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
**S1–S3 pushed; the live-fix task may start.**
|
||||
|
||||
> This report is `REPORT-media-all-drives-2026-10-14.md`, not the root `REPORT.md`: the repo rule (`CLAUDE.md`, „two
|
||||
> sessions in this repo clobber each other") sends a parallel session to a topic file. The task asked for the line at
|
||||
> the top of `REPORT.md`; it is here instead.
|
||||
|
||||
**Resume point:** Parts A–C done and pushed. Next: Part D (Plex path check, the test sheet, then STOP for the operator).
|
||||
|
||||
# Media apps on every drive — Tester 1 rig (2026-10-14)
|
||||
|
||||
Evidence: `documentation/audits/media-all-drives-2026-10-14/README.md` (one row per part).
|
||||
|
||||
## In plain words
|
||||
|
||||
1. **The chain works with the new folder layout.** A film went from the download app to the library on the same
|
||||
drive as one file with two names. No extra copy. Through both download apps.
|
||||
2. **Today's layout cannot work.** The download apps save where Sonarr and Radarr never look. And even with the same
|
||||
folders, two separate mounts force a full copy. Measured.
|
||||
3. **Each film can go to its own drive** by a tag. A wrong tag still works, but makes a full copy across drives.
|
||||
4. **No test film reached any backup.** Proven three ways before the first download.
|
||||
5. **A power cut hit Tester 1 at 17:25.** It came from another session's test on the HP box. Tester 1 came back by
|
||||
itself.
|
||||
|
||||
## Claims in the task that turned out wrong
|
||||
|
||||
- „`media/` only keeps photos and documents out of these apps' reach" — `media/` holds `photos`, `books`,
|
||||
`audiobooks` and more in the skeleton. Documents are outside it; photos are not.
|
||||
- R-615: removing the cache is no longer needed (a changed `repo_url` re-clones, seen live).
|
||||
- The `/media/shows` copy is in Plex too, not only Sonarr and Jellyfin (R-1015).
|
||||
|
||||
## Register
|
||||
|
||||
Rows before 133 → after 139 · opened 6 (R-1014..R-1019) · closed 0.
|
||||
@@ -0,0 +1,9 @@
|
||||
== /api/system/info 200
|
||||
{"ok": true, "data": {"sync_status": {"last_sync": "2026-10-14T13:44:42.082522426Z", "last_status": "ok", "syncing": false}, "system": {"total_mem_mb": 4096, "used_mem_mb": 680, "avail_mem_mb": 3416, "mem_percent": 16.6015625, "disk_total_gb": 68.72599792480469, "disk_used_gb": 4.120944976806641, "disk_avail_gb": 61.089439392089844, "disk_percent": 6.319461258646124, "disk_known": true, "hdd_total_gb": 48.914520263671875, "hdd_used_gb": 0.23301315307617188, "hdd_avail_gb": 46.1658821105957, "hdd_percent": 0.5021954763190453, "hdd_configured": true, "hdd_known": true, "cpu_percent": 17.57910597689603, "load_avg_1": 0.21, "load_avg_5": 0.3, "load_avg_15": 0.32, "temperature_celsius": 0}}}
|
||||
== /api/system/memory 200
|
||||
{"data": {"vmid": 9201, "allocated_mb": 4096, "usage_mb": 944, "host_total_mb": 7872, "min_mb": 2048, "max_mb": 5824, "floor_mb": 2048, "running": true}, "ok": true}
|
||||
== /api/disks 200
|
||||
{"data": {"vmid": 9201, "disks": [{"name": "863c67fd-3e21-4d9f-9a74-3c5fa406f2a2", "type": "usb", "state": "attached", "backing_device": "/dev/sdb", "mount_path": "/mnt/adatlemez", "class": "", "role": "user-data", "data_bearing": false, "data_reason": "", "total_bytes": 52521566208, "used_bytes": 250195968, "used_fraction": 0.0047636806375718965, "durable_id": "uuid:863c67fd-3e21-4d9f-9a74-3c5fa406f2a2", "guest_attached": false, "guest_path": "/mnt/felhom-drives/adatlemez", "bound_under_parent": true, "smart": {"health": "UNKNOWN", "model_name": "QEMU QEMU HARDDISK", "temperature_c": 0, "power_on_hours": null, "reallocated_sectors": null, "pending_sectors": null, "offline_uncorrectable": null, "critical_warning": null, "media_errors": null, "percentage_used": null}}, {"name": "local", "type": "local", "state": "attached", "backing_device": "", "mount_path": "", "class": "", "role": "system", "data_bearing": false, "data_reason": "", "total_bytes": 62903865344, "used_bytes": 13649629184, "used_fraction": 0.216991898818217, "durable_id": "path:/var/lib/vz", "guest_attached": false, "backup_target": true, "bound_under_parent": false, "smart": {"health": "UNKNOWN", "model_name": "QEMU QEMU HARDDISK", "temperature_c": 0, "power_on_hours": null, "reallocated_sectors": null, "pending_sectors": null, "offline_uncorrectable": null, "critical_warning": null, "media_errors": null, "percentage_used": null}}, {"name": "local-lvm", "type": "lvmthin", "state": "attached", "backing_device": "", "mount_path": "", "class": "", "role": "system", "data_bearing": false, "data_reason": "", "total_bytes": 121521569792, "used_bytes": 7813836937, "used_fraction": 0.0642999999948519, "durable_id": "pve/data", "guest_attached": false, "bound_under_parent": false, "smart": {"health": "UNKNOWN", "temperature_c": null, "power_on_hours": null, "reallocated_sectors": null, "pending_sectors": null, "offline_uncorrectable": null, "critical_warning": null, "media_errors": null, "percentage_used": null}}, {"name": "felhom-pbs", "type": "pbs", "state": "attached", "backing_device": "", "mount_path": "", "class": "", "role": "backup", "data_bearing": false, "data_reason": "", "total_bytes": 0, "used_bytes": 0, "used_fraction": 0, "durable_id": "10.77.0.1:felhom-offsite#c6:07:28:3f:5b:7b:5a:41:90:28:d7:ca:4f:37:14:70:56:39:2e:2f:0b:71:e8:06:ca:60:4a:d5:56:5f:3c:fd", "guest_attached": false, "bound_under_parent": false, "smart": {"health": "UNKNOWN", "temperature_c": null, "power_on_hours": nul
|
||||
== /api/storage/backup-target 200
|
||||
{"data": {"degraded": true, "known": true, "label": "", "message": "A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer — így hibás fájlok ellen véd, lemezhiba ellen nem. Csatlakoztass egy második meghajtót a teljes védelemhez.", "offer_label": "Adatlemez", "offer_message": "Ezt a meghajtót kijelölheted a rendszermentés helyéül — így egy lemezhiba után is vissza tudod állítani a rendszert.", "offer_path": "/mnt/felhom-drives/adatlemez", "target": "local"}, "ok": true}
|
||||
== deployed: [('bookstack', 'running'), ('paperless-ngx', 'running'), ('privatebin', 'running')]
|
||||
@@ -0,0 +1,30 @@
|
||||
2026-10-14T15:57:57+02:00
|
||||
bios: ovmf
|
||||
boot: order=scsi0
|
||||
cores: 4
|
||||
cpu: host
|
||||
efidisk0: nvme-scratch:341/vm-341-disk-0.raw,efitype=4m,pre-enrolled-keys=0,size=528K
|
||||
machine: q35
|
||||
memory: 8192
|
||||
meta: creation-qemu=11.0.0,ctime=1791141969
|
||||
name: night1004-tester1
|
||||
net0: virtio=BC:24:11:AC:E3:F9,bridge=vmbr0
|
||||
onboot: 1
|
||||
ostype: l26
|
||||
scsi0: nvme-scratch:341/vm-341-disk-1.raw,size=200G
|
||||
scsi1: nvme-scratch:341/vm-341-disk-2.raw,size=50G
|
||||
scsihw: virtio-scsi-single
|
||||
smbios1: uuid=451c8d94-abf2-46e4-a278-b579a9f315b5
|
||||
vga: std
|
||||
vmgenid: acdf89db-a810-445c-9d88-7bccd688d3c1
|
||||
Filesystem 1B-blocks Used Available Use% Mounted on
|
||||
/dev/nvme0n1 1006980812800 135772049408 819981512704 15% /mnt/hdd_1
|
||||
total 76833588
|
||||
4 drwxr----- 2 root root 4096 Oct 4 21:26 .
|
||||
4 drwxr-xr-x 5 root root 4096 Oct 14 05:42 ..
|
||||
528 -rw-r----- 1 root root 540672 Oct 14 14:07 vm-341-disk-0.raw
|
||||
75117456 -rw-r----- 1 root root 214748364800 Oct 14 15:57 vm-341-disk-1.raw
|
||||
1715596 -rw-r----- 1 root root 53687091200 Oct 14 15:57 vm-341-disk-2.raw
|
||||
total used free shared buff/cache available
|
||||
Mem: 29994 11576 10048 164 8973 18417
|
||||
Swap: 8191 0 8191
|
||||
@@ -0,0 +1,49 @@
|
||||
2026-10-14T15:57:59+02:00
|
||||
felhom-agent 0.156.1
|
||||
arch: amd64
|
||||
cores: 3
|
||||
features: nesting=1,keyctl=1
|
||||
hookscript: local:snippets/felhom-guest-hook.sh
|
||||
hostname: tester-1
|
||||
memory: 4096
|
||||
mp0: local-lvm:vm-9201-disk-1,mp=/var/lib/felhom,backup=1,size=70G
|
||||
mp8: /mnt/felhom-drives,mp=/mnt/felhom-drives
|
||||
mp9: /var/lib/felhom-agent/guests/9201/bootstrap,mp=/etc/felhom-bootstrap,ro=1
|
||||
net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:FF:2F:FC,ip=dhcp,type=veth
|
||||
net1: name=eth1,bridge=vmbr9,hwaddr=BC:24:11:6C:BB:E5,ip=169.254.253.2/30,type=veth
|
||||
onboot: 1
|
||||
ostype: debian
|
||||
rootfs: local-lvm:vm-9201-disk-0,size=32G
|
||||
swap: 512
|
||||
unprivileged: 1
|
||||
NAME SIZE TYPE MOUNTPOINT SERIAL
|
||||
sda 200G disk drive-scsi0
|
||||
|-sda1 1007K part
|
||||
|-sda2 1G part /boot/efi
|
||||
`-sda3 199G part
|
||||
|-pve-swap 7.7G lvm [SWAP]
|
||||
|-pve-root 59.8G lvm /
|
||||
|-pve-data_tmeta 1.2G lvm
|
||||
| `-pve-data-tpool 113.2G lvm
|
||||
| |-pve-data 113.2G lvm
|
||||
| |-pve-vm--9201--disk--0 32G lvm
|
||||
| `-pve-vm--9201--disk--1 70G lvm
|
||||
`-pve-data_tdata 113.2G lvm
|
||||
`-pve-data-tpool 113.2G lvm
|
||||
|-pve-data 113.2G lvm
|
||||
|-pve-vm--9201--disk--0 32G lvm
|
||||
`-pve-vm--9201--disk--1 70G lvm
|
||||
sdb 50G disk /mnt/adatlemez drive-scsi1
|
||||
total used free shared buff/cache available
|
||||
Mem: 7872 2668 2247 105 3333 5203
|
||||
Swap: 7871 0 7871
|
||||
felhom-controller gitea.dooplex.hu/admin/felhom-controller:0.310.0 Up About an hour (healthy)
|
||||
paperless-webserver ghcr.io/paperless-ngx/paperless-ngx:2.20.15 Up 2 hours (healthy)
|
||||
paperless-postgres postgres:18-alpine Up 2 hours (healthy)
|
||||
paperless-redis redis:7-alpine Up 2 hours (healthy)
|
||||
privatebin privatebin/pdo:2.0.6 Up 2 hours (healthy)
|
||||
bookstack lscr.io/linuxserver/bookstack:26.09.1 Up 2 hours (healthy)
|
||||
bookstack-db mariadb:12.3 Up 2 hours (healthy)
|
||||
filebrowser gtstef/filebrowser:1.5.6-stable Up 2 hours (healthy)
|
||||
cloudflared cloudflare/cloudflared:2026.9.3 Up 2 hours (healthy)
|
||||
traefik traefik:v3.7.13 Up 2 hours
|
||||
@@ -0,0 +1,33 @@
|
||||
+ date -Is
|
||||
2026-10-14T15:58:36+02:00
|
||||
+ df -B1G /mnt/hdd_1
|
||||
+ tail -1
|
||||
/dev/nvme0n1 938 127 764 15% /mnt/hdd_1
|
||||
+ qm set 341 --scsi2 nvme-scratch:100,backup=0
|
||||
update VM 341: -scsi2 nvme-scratch:100,backup=0
|
||||
Formatting '/mnt/hdd_1/images/341/vm-341-disk-3.raw', fmt=raw size=107374182400 preallocation=off
|
||||
scsi2: successfully created disk 'nvme-scratch:341/vm-341-disk-3.raw,backup=0,size=100G'
|
||||
+ qm set 341 --scsi3 nvme-scratch:100,backup=0
|
||||
update VM 341: -scsi3 nvme-scratch:100,backup=0
|
||||
Formatting '/mnt/hdd_1/images/341/vm-341-disk-4.raw', fmt=raw size=107374182400 preallocation=off
|
||||
scsi3: successfully created disk 'nvme-scratch:341/vm-341-disk-4.raw,backup=0,size=100G'
|
||||
+ qm config 341
|
||||
+ grep -E '^scsi|^memory|hotplug'
|
||||
memory: 8192
|
||||
scsi0: nvme-scratch:341/vm-341-disk-1.raw,size=200G
|
||||
scsi1: nvme-scratch:341/vm-341-disk-2.raw,size=50G
|
||||
scsi2: nvme-scratch:341/vm-341-disk-3.raw,backup=0,size=100G
|
||||
scsi3: nvme-scratch:341/vm-341-disk-4.raw,backup=0,size=100G
|
||||
scsihw: virtio-scsi-single
|
||||
+ ls -las /mnt/hdd_1/images/341/
|
||||
total 76833596
|
||||
4 drwxr----- 2 root root 4096 Oct 14 15:58 .
|
||||
4 drwxr-xr-x 5 root root 4096 Oct 14 05:42 ..
|
||||
528 -rw-r----- 1 root root 540672 Oct 14 14:07 vm-341-disk-0.raw
|
||||
75117456 -rw-r----- 1 root root 214748364800 Oct 14 15:58 vm-341-disk-1.raw
|
||||
1715596 -rw-r----- 1 root root 53687091200 Oct 14 15:58 vm-341-disk-2.raw
|
||||
4 -rw-r----- 1 root root 107374182400 Oct 14 15:58 vm-341-disk-3.raw
|
||||
4 -rw-r----- 1 root root 107374182400 Oct 14 15:58 vm-341-disk-4.raw
|
||||
+ df -B1G /mnt/hdd_1
|
||||
+ tail -1
|
||||
/dev/nvme0n1 938 127 764 15% /mnt/hdd_1
|
||||
@@ -0,0 +1,5 @@
|
||||
15:59:08 candidates -> 200 ['/dev/sdc', '/dev/sdd']
|
||||
15:59:08 POST /api/storage/init {"device": "/dev/sdc", "fstype": "ext4", "mount_name": "media_a", "label": "Media A", "set_default": false, "confirmed": false, "durable_id": ""} -> 200 {"data": {"phase": "formatting", "started": true}, "ok": true}
|
||||
15:59:10 status -> 200 {"device": "/dev/sdc", "durable_id": "", "error": "", "opsign": "", "phase": "mounting", "reason": "", "started_at": "2026-10-14T13:59:08.76516334Z", "updated_at": "2026-10-14T13:59:09.122147197Z", "where": "/mnt/media_a"}
|
||||
15:59:12 status -> 200 {"device": "/dev/sdc", "durable_id": "", "error": "", "opsign": "", "phase": "done", "reason": "", "started_at": "2026-10-14T13:59:08.76516334Z", "updated_at": "2026-10-14T13:59:10.953891979Z", "where": "/mnt/felhom-drives/media_a"}
|
||||
RESULT done
|
||||
@@ -0,0 +1,5 @@
|
||||
15:59:16 candidates -> 200 ['/dev/sdd']
|
||||
15:59:16 POST /api/storage/init {"device": "/dev/sdd", "fstype": "ext4", "mount_name": "media_b", "label": "Media B", "set_default": false, "confirmed": false, "durable_id": ""} -> 200 {"data": {"phase": "formatting", "started": true}, "ok": true}
|
||||
15:59:18 status -> 200 {"device": "/dev/sdd", "durable_id": "", "error": "", "opsign": "", "phase": "mounting", "reason": "", "started_at": "2026-10-14T13:59:16.535337994Z", "updated_at": "2026-10-14T13:59:16.879143306Z", "where": "/mnt/media_b"}
|
||||
15:59:20 status -> 200 {"device": "/dev/sdd", "durable_id": "", "error": "", "opsign": "", "phase": "done", "reason": "", "started_at": "2026-10-14T13:59:16.535337994Z", "updated_at": "2026-10-14T13:59:18.641630025Z", "where": "/mnt/felhom-drives/media_b"}
|
||||
RESULT done
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
backup-target 200 {"data": {"degraded": true, "known": true, "label": "", "message": "A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer — így hibás fájlok ellen véd, lemezhiba ellen nem. Csatlakoztass egy második meghajtót a teljes védelemhez.", "offer_label": "Adatlemez", "offer_message": "Ezt a meghajtót kijelölheted a rendszermentés helyéül — így egy lemezhiba után is vissza tudod állítani a rendszert.", "offer_path": "/mnt/felhom-drives/adatlemez", "target": "local"}, "ok": true}
|
||||
storage page 200 77
|
||||
Tárhely — Felhom.eu
|
||||
Indítópult
|
||||
Vezérlőpult
|
||||
Alkalmazások
|
||||
Tárhely
|
||||
Meghajtók
|
||||
Hálózati tárhely
|
||||
Biztonsági mentés
|
||||
Áttekintés
|
||||
Távoli mentés
|
||||
Alkalmazások
|
||||
Visszaállítás
|
||||
Megosztás
|
||||
Hálózati megosztás
|
||||
Rendszermonitor
|
||||
Hibakeresés
|
||||
Beállítások
|
||||
Rendszer
|
||||
Értesítések
|
||||
Biztonság és hozzáférés
|
||||
0.310.0
|
||||
Magyar
|
||||
English
|
||||
Kijelentkezés ↗
|
||||
Tárhely — Meghajtók
|
||||
Adattárolók
|
||||
Külső meghajtók kezelése alkalmazásadatok tárolásához.
|
||||
Megőrzött adatok
|
||||
Eltávolított alkalmazások lemezen maradt adatai
|
||||
Adatlemez
|
||||
/mnt/felhom-drives/adatlemez
|
||||
Alapértelmezett
|
||||
Aktív
|
||||
0,2 GB / 48,9 GB
|
||||
ext4 · /dev/sdb[/felhom-data] · QEMU HARDDISK
|
||||
1 alkalmazás használja
|
||||
Paperless-ngx
|
||||
6,3 KB
|
||||
Mozgatás
|
||||
Új telepítések letiltása
|
||||
Áthelyezés ide…
|
||||
Media A (/mnt/felhom-drives/media_a)
|
||||
Media B (/mnt/felhom-drives/media_b)
|
||||
Összes adat áthelyezése
|
||||
Végleges leszerelés
|
||||
Media A
|
||||
/mnt/felhom-drives/media_a
|
||||
Aktív
|
||||
0,0 GB / 97,9 GB
|
||||
ext4 · /dev/sdc[/felhom-data] · QEMU HARDDISK
|
||||
Nincs alkalmazás ezen a tárolón
|
||||
Legyen alapértelmezett
|
||||
Új telepítések letiltása
|
||||
Végleges leszerelés
|
||||
Media B
|
||||
/mnt/felhom-drives/media_b
|
||||
Aktív
|
||||
0,0 GB / 97,9 GB
|
||||
ext4 · /dev/sdd[/felhom-data] · QEMU HARDDISK
|
||||
Nincs alkalmazás ezen a tárolón
|
||||
Legyen alapértelmezett
|
||||
Új telepítések letiltása
|
||||
Végleges leszerelés
|
||||
Adatok áthelyezése
|
||||
…
|
||||
Új meghajtó inicializálása
|
||||
Meglévő meghajtó csatolása
|
||||
Nem regisztrált meghajtók
|
||||
az ügynök által észlelt, még nem regisztrált adatmeghajtók
|
||||
Rendszermeghajtók
|
||||
védett — csak operátori aláírással módosíthatók
|
||||
Már csatlakoztatott tárhely hozzáadása kézzel
|
||||
Elérési út
|
||||
Pl. /mnt/hdd_1 — a meghajtónak már csatolva kell lennie
|
||||
Megnevezés (opcionális)
|
||||
Legyen alapértelmezett új telepítéseknél
|
||||
Hozzáadás
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
Wed Oct 14 13:59:31 UTC 2026
|
||||
TARGET SOURCE FSTYPE PROPAGATION
|
||||
/mnt/felhom-drives /dev/mapper/pve-root[/mnt/felhom-drives] ext4 shared,slave
|
||||
|-/mnt/felhom-drives/adatlemez /dev/sdb[/felhom-data] ext4 shared,slave
|
||||
|-/mnt/felhom-drives/media_a /dev/sdc[/felhom-data] ext4 shared,slave
|
||||
`-/mnt/felhom-drives/media_b /dev/sdd[/felhom-data] ext4 shared,slave
|
||||
/mnt/felhom-drives /dev/mapper/pve-root[/mnt/felhom-drives] ext4 shared,slave
|
||||
|-/mnt/felhom-drives/adatlemez /dev/sdb[/felhom-data] ext4 shared,slave
|
||||
|-/mnt/felhom-drives/media_a /dev/sdc[/felhom-data] ext4 shared,slave
|
||||
`-/mnt/felhom-drives/media_b /dev/sdd[/felhom-data] ext4 shared,slave
|
||||
== media_a
|
||||
total 12
|
||||
drwxr-xr-x 3 root root 4096 Oct 14 13:59 .
|
||||
drwxr-xr-x 5 nobody nogroup 4096 Oct 14 13:59 ..
|
||||
drwxrwsr-x 6 root 1000 4096 Oct 14 13:59 userdata
|
||||
/mnt/felhom-drives/media_a
|
||||
/mnt/felhom-drives/media_a/userdata
|
||||
/mnt/felhom-drives/media_a/userdata/documents
|
||||
/mnt/felhom-drives/media_a/userdata/downloads
|
||||
/mnt/felhom-drives/media_a/userdata/media
|
||||
/mnt/felhom-drives/media_a/userdata/media/audiobooks
|
||||
/mnt/felhom-drives/media_a/userdata/media/books
|
||||
/mnt/felhom-drives/media_a/userdata/media/comics
|
||||
/mnt/felhom-drives/media_a/userdata/media/grimmory
|
||||
/mnt/felhom-drives/media_a/userdata/media/metube
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies
|
||||
/mnt/felhom-drives/media_a/userdata/media/music
|
||||
/mnt/felhom-drives/media_a/userdata/media/nzbget
|
||||
/mnt/felhom-drives/media_a/userdata/media/photos
|
||||
/mnt/felhom-drives/media_a/userdata/media/podcasts
|
||||
/mnt/felhom-drives/media_a/userdata/media/torrents
|
||||
/mnt/felhom-drives/media_a/userdata/media/transmission
|
||||
/mnt/felhom-drives/media_a/userdata/media/tv
|
||||
/mnt/felhom-drives/media_a/userdata/media/usenet
|
||||
/mnt/felhom-drives/media_a/userdata/roms
|
||||
== media_b
|
||||
total 12
|
||||
drwxr-xr-x 3 root root 4096 Oct 14 13:59 .
|
||||
drwxr-xr-x 5 nobody nogroup 4096 Oct 14 13:59 ..
|
||||
drwxrwsr-x 6 root 1000 4096 Oct 14 13:59 userdata
|
||||
/mnt/felhom-drives/media_b
|
||||
/mnt/felhom-drives/media_b/userdata
|
||||
/mnt/felhom-drives/media_b/userdata/documents
|
||||
/mnt/felhom-drives/media_b/userdata/downloads
|
||||
/mnt/felhom-drives/media_b/userdata/media
|
||||
/mnt/felhom-drives/media_b/userdata/media/audiobooks
|
||||
/mnt/felhom-drives/media_b/userdata/media/books
|
||||
/mnt/felhom-drives/media_b/userdata/media/comics
|
||||
/mnt/felhom-drives/media_b/userdata/media/grimmory
|
||||
/mnt/felhom-drives/media_b/userdata/media/metube
|
||||
/mnt/felhom-drives/media_b/userdata/media/movies
|
||||
/mnt/felhom-drives/media_b/userdata/media/music
|
||||
/mnt/felhom-drives/media_b/userdata/media/nzbget
|
||||
/mnt/felhom-drives/media_b/userdata/media/photos
|
||||
/mnt/felhom-drives/media_b/userdata/media/podcasts
|
||||
/mnt/felhom-drives/media_b/userdata/media/torrents
|
||||
/mnt/felhom-drives/media_b/userdata/media/transmission
|
||||
/mnt/felhom-drives/media_b/userdata/media/tv
|
||||
/mnt/felhom-drives/media_b/userdata/media/usenet
|
||||
/mnt/felhom-drives/media_b/userdata/roms
|
||||
== adatlemez
|
||||
total 20
|
||||
drwxr-xr-x 5 root root 4096 Oct 4 19:47 .
|
||||
drwxr-xr-x 5 nobody nogroup 4096 Oct 14 13:59 ..
|
||||
drwxr-xr-x 3 root root 4096 Oct 4 19:45 appdata
|
||||
drwxr-xr-x 5 root root 4096 Oct 5 03:30 backups
|
||||
drwxrwsr-x 6 root 1000 4096 Oct 4 19:44 userdata
|
||||
/mnt/felhom-drives/adatlemez
|
||||
/mnt/felhom-drives/adatlemez/appdata
|
||||
/mnt/felhom-drives/adatlemez/appdata/paperless
|
||||
/mnt/felhom-drives/adatlemez/appdata/paperless/export
|
||||
/mnt/felhom-drives/adatlemez/appdata/paperless/media
|
||||
/mnt/felhom-drives/adatlemez/backups
|
||||
/mnt/felhom-drives/adatlemez/backups/offsite-proof
|
||||
/mnt/felhom-drives/adatlemez/backups/primary
|
||||
/mnt/felhom-drives/adatlemez/backups/primary/paperless-ngx
|
||||
/mnt/felhom-drives/adatlemez/backups/secondary
|
||||
/mnt/felhom-drives/adatlemez/backups/secondary/bookstack
|
||||
/mnt/felhom-drives/adatlemez/backups/secondary/privatebin
|
||||
/mnt/felhom-drives/adatlemez/userdata
|
||||
/mnt/felhom-drives/adatlemez/userdata/documents
|
||||
/mnt/felhom-drives/adatlemez/userdata/downloads
|
||||
/mnt/felhom-drives/adatlemez/userdata/media
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/audiobooks
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/books
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/comics
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/grimmory
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/metube
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/movies
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/music
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/nzbget
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/photos
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/podcasts
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/torrents
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/transmission
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/tv
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/usenet
|
||||
/mnt/felhom-drives/adatlemez/userdata/roms
|
||||
2026-10-14T13:59:18.752620558Z /mnt/felhom-drives/adatlemez/userdata->/srv/adatlemez true rprivate; /mnt/sys_drive/felhom-data/userdata/import->/srv/beolvasas true rprivate; /mnt/felhom-drives/media_a/userdata->/srv/media_a true rprivate; /mnt/felhom-drives/media_b/userdata->/srv/media_b true rprivate; /opt/docker/stacks/filebrowser/config.yaml->/home/filebrowser/config.yaml false rprivate; /var/lib/docker/volumes/filebrowser_filebrowser_data/_data->/home/filebrowser/data true ;
|
||||
2026/10/14 13:55:42 [INFO] [scheduler] Job agent-channel-health completed (took 707ms)
|
||||
2026/10/14 13:55:51 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:00 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:56:01 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:01 [INFO] Health probes: 1 ok (of 1 probed)
|
||||
2026/10/14 13:56:11 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:21 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:21 [INFO] Health probes: 2 ok (of 2 probed)
|
||||
2026/10/14 13:56:31 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:32 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:56:41 [INFO] [scheduler] Running job: stack-scan
|
||||
2026/10/14 13:56:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:41 [INFO] [stacks] ScanStacks complete: 67 stacks found (3 deployed, 64 available)
|
||||
2026/10/14 13:56:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:41 [INFO] [scheduler] Job stack-scan completed (took 125ms)
|
||||
2026/10/14 13:56:41 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/10/14 13:56:42 [INFO] [scheduler] Job agent-channel-health completed (took 485ms)
|
||||
2026/10/14 13:56:51 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:01 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:11 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:21 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:31 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:41 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/10/14 13:57:42 [INFO] [scheduler] Job agent-channel-health completed (took 473ms)
|
||||
2026/10/14 13:57:51 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:01 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:11 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:21 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:31 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:41 [INFO] [scheduler] Running job: stack-scan
|
||||
2026/10/14 13:58:41 [INFO] [stacks] ScanStacks complete: 67 stacks found (3 deployed, 64 available)
|
||||
2026/10/14 13:58:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:41 [INFO] [scheduler] Job stack-scan completed (took 81ms)
|
||||
2026/10/14 13:58:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:41 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/10/14 13:58:42 [INFO] [scheduler] Job agent-channel-health completed (took 480ms)
|
||||
2026/10/14 13:58:49 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:58:51 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:59:01 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:59:08 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:59:08 [INFO] [web] storage init "/mnt/media_a" started (device /dev/sdc, fs ext4)
|
||||
2026/10/14 13:59:09 [INFO] [web] enroll /dev/sdc: mounting the agent-verified new filesystem 4ebfd36d-87d3-4b63-839e-a5a710fc799a
|
||||
2026/10/14 13:59:10 [INFO] [web] enroll: drive bound into guest: /mnt/media_a
|
||||
2026/10/14 13:59:10 [INFO] [settings] Added storage path: /mnt/felhom-drives/media_a
|
||||
2026/10/14 13:59:10 [INFO] [settings] Settings saved
|
||||
2026/10/14 13:59:10 [INFO] [web] storage init done: /mnt/felhom-drives/media_a registered (2188ms)
|
||||
2026/10/14 13:59:10 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/paperless-ngx/docker-compose.yml
|
||||
2026/10/14 13:59:11 [INFO] [web] FileBrowser mounts synced (recreated) — 2 storage path(s), config updated
|
||||
2026/10/14 13:59:11 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:59:16 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:59:16 [INFO] [web] storage init "/mnt/media_b" started (device /dev/sdd, fs ext4)
|
||||
2026/10/14 13:59:16 [INFO] [web] enroll /dev/sdd: mounting the agent-verified new filesystem e15652d5-0aa7-4eb8-93a1-afb2839d527c
|
||||
2026/10/14 13:59:18 [INFO] [web] enroll: drive bound into guest: /mnt/media_b
|
||||
2026/10/14 13:59:18 [INFO] [settings] Added storage path: /mnt/felhom-drives/media_b
|
||||
2026/10/14 13:59:18 [INFO] [settings] Settings saved
|
||||
2026/10/14 13:59:18 [INFO] [web] storage init done: /mnt/felhom-drives/media_b registered (2106ms)
|
||||
2026/10/14 13:59:18 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/paperless-ngx/docker-compose.yml
|
||||
2026/10/14 13:59:19 [INFO] [web] FileBrowser mounts synced (recreated) — 3 storage path(s), config updated
|
||||
2026/10/14 13:59:21 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
2026-10-14T15:59:52+02:00
|
||||
--- cluster backup jobs:
|
||||
[]
|
||||
|
||||
ls: cannot access '/etc/pve/jobs.cfg': No such file or directory
|
||||
scsi2: nvme-scratch:341/vm-341-disk-3.raw,backup=0,size=100G
|
||||
scsi3: nvme-scratch:341/vm-341-disk-4.raw,backup=0,size=100G
|
||||
@@ -0,0 +1,32 @@
|
||||
2026-10-14T15:59:55+02:00
|
||||
mp0: local-lvm:vm-9201-disk-1,mp=/var/lib/felhom,backup=1,size=70G
|
||||
mp8: /mnt/felhom-drives,mp=/mnt/felhom-drives
|
||||
mp9: /var/lib/felhom-agent/guests/9201/bootstrap,mp=/etc/felhom-bootstrap,ro=1
|
||||
rootfs: local-lvm:vm-9201-disk-0,size=32G
|
||||
total 4
|
||||
-rw-r--r-- 1 root root 1495 Oct 14 04:44 lxc-9201.log
|
||||
2026-10-14 04:42:22 INFO: Starting Backup of VM 9201 (lxc)
|
||||
2026-10-14 04:42:22 INFO: including mount point rootfs ('/') in backup
|
||||
2026-10-14 04:42:22 INFO: including mount point mp0 ('/var/lib/felhom') in backup
|
||||
2026-10-14 04:42:22 INFO: excluding bind mount point mp8 ('/mnt/felhom-drives') from backup (not a volume)
|
||||
2026-10-14 04:42:22 INFO: excluding bind mount point mp9 ('/etc/felhom-bootstrap') from backup (not a volume)
|
||||
2026-10-14 04:42:22 INFO: backup mode: snapshot
|
||||
2026-10-14 04:42:24 INFO: creating vzdump archive '/var/lib/vz/dump/vzdump-lxc-9201-2026_10_14-04_42_22.tar.zst'
|
||||
2026-10-14 04:44:52 INFO: Total bytes written: 5447720960 (5.1GiB, 35MiB/s)
|
||||
2026-10-14 04:44:53 INFO: Finished Backup of VM 9201 (00:02:31)
|
||||
total 5245972
|
||||
-rw-r--r-- 1 root root 1495 Oct 14 04:44 vzdump-lxc-9201-2026_10_14-04_42_22.log
|
||||
-rw-r--r-- 1 root root 16 Oct 14 04:44 vzdump-lxc-9201-2026_10_14-04_42_22.tar.zst.notes
|
||||
-rw-r--r-- 1 root root 1575398000 Oct 14 04:44 vzdump-lxc-9201-2026_10_14-04_42_22.tar.zst
|
||||
-rw-r--r-- 1 root root 1495 Oct 13 04:40 vzdump-lxc-9201-2026_10_13-04_38_03.log
|
||||
== UPID:felhom:0014BF84:005ED7D4:6ACBEB45:vzdump:9201:felhom-agent@pve!agent:
|
||||
INFO: starting new backup job: vzdump 9201 --notes-template 'felhom local-api' --compress zstd --mode snapshot --storage felhom-pbs --node felhom
|
||||
INFO: Starting Backup of VM 9201 (lxc)
|
||||
INFO: including mount point rootfs ('/') in backup
|
||||
INFO: including mount point mp0 ('/var/lib/felhom') in backup
|
||||
INFO: excluding bind mount point mp8 ('/mnt/felhom-drives') from backup (not a volume)
|
||||
INFO: excluding bind mount point mp9 ('/etc/felhom-bootstrap') from backup (not a volume)
|
||||
INFO: Starting backup: [tester-1]:ct/9201/2026-10-11T20:02:13Z
|
||||
INFO: Starting backup protocol: Sun Oct 11 22:02:14 2026
|
||||
INFO: Finished Backup of VM 9201 (00:00:58)
|
||||
TASK OK
|
||||
@@ -0,0 +1,50 @@
|
||||
# A6(3) — the PRODUCTION classifier (controller main acbbf4b = 0.310.0 source) over template dirs; nsRoot = drive A
|
||||
# DRILL templates (the layout under test):
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/sonarr backup-block=false classified-binds=0 hasClassification=false
|
||||
tier2-secondary: hasClassification=false paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=false paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/radarr backup-block=false classified-binds=0 hasClassification=false
|
||||
tier2-secondary: hasClassification=false paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=false paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/qbittorrent backup-block=true classified-binds=0 hasClassification=true
|
||||
tier2-secondary: hasClassification=true paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=true paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/transmission backup-block=true classified-binds=0 hasClassification=true
|
||||
tier2-secondary: hasClassification=true paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=true paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/plex backup-block=false classified-binds=0 hasClassification=false
|
||||
tier2-secondary: hasClassification=false paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=false paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/jellyfin backup-block=false classified-binds=0 hasClassification=false
|
||||
tier2-secondary: hasClassification=false paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=false paths=0 skipped=0
|
||||
|
||||
# CONTROL 1 — LIVE templates (today's shape):
|
||||
== /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/templates/sonarr backup-block=true classified-binds=2 hasClassification=true
|
||||
bind root=userdata rel="media/tv" ro=false class="excluded" origin=explicit
|
||||
bind root=userdata rel="downloads" ro=false class="excluded" origin=explicit
|
||||
tier2-secondary: hasClassification=true paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=true paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/templates/qbittorrent backup-block=true classified-binds=1 hasClassification=true
|
||||
bind root=userdata rel="media/torrents" ro=false class="optional" origin=explicit
|
||||
tier2-secondary: hasClassification=true paths=1 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/torrents (optional)
|
||||
tier3-offsite: hasClassification=true paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/templates/transmission backup-block=true classified-binds=2 hasClassification=true
|
||||
bind root=userdata rel="media/transmission" ro=false class="optional" origin=explicit
|
||||
bind root=userdata rel="media/transmission/watch" ro=false class="mandatory" origin=default_writable
|
||||
tier2-secondary: hasClassification=true paths=1 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/transmission (optional)
|
||||
tier3-offsite: hasClassification=true paths=1 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/transmission/watch (mandatory)
|
||||
|
||||
# CONTROL 2 (decoy, must show CAPTURED mandatory) — live sonarr with a backup block that does NOT list its writable ${USERDATA_PATH} binds:
|
||||
== /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/bf86b82e-7538-4da2-99f7-a6f96249f7fb/scratchpad/decoy/sonarr-unlisted backup-block=true classified-binds=2 hasClassification=true
|
||||
bind root=userdata rel="media/tv" ro=false class="mandatory" origin=default_writable
|
||||
bind root=userdata rel="downloads" ro=false class="mandatory" origin=default_writable
|
||||
tier2-secondary: hasClassification=true paths=2 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/downloads (mandatory)
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/tv (mandatory)
|
||||
tier3-offsite: hasClassification=true paths=2 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/downloads (mandatory)
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/tv (mandatory)
|
||||
@@ -0,0 +1,19 @@
|
||||
# Predictions — written before Part A4/A5/A6 ran (2026-10-14 ~16:00 CEST)
|
||||
|
||||
A4 enrolment
|
||||
- Both disks enrol through /api/storage/init (ext4) and mount at /mnt/felhom-drives/media_a and media_b in the guest.
|
||||
- Neither becomes the Tier-1 backup target: the target role is written ONLY by /api/storage/backup-target/assign
|
||||
(the offer's acceptance), never by registration. The target stays `local`.
|
||||
- Neither becomes the default storage: this run sends set_default=false (the wizard's checkbox is ON by default).
|
||||
|
||||
A5 skeleton and file browser
|
||||
- Both drives get felhom-data/userdata/{media/{movies,tv,music,...},downloads,...}.
|
||||
- FileBrowser's container is recreated and binds both new drives' userdata/.
|
||||
- The dashboard says nothing about apps needing a restart (no such notice exists in v0.310.0).
|
||||
|
||||
A6 backups
|
||||
- (1) demo-hp: scsi2/scsi3 carry backup=0 (read back), and demo-hp has no vzdump job for VM 341 at all.
|
||||
- (2) Tester 1's whole-guest backup (vzdump of 9201) does not include /mnt/felhom-drives: mp8 is a host bind mount,
|
||||
out of vzdump scope (07 §6.1 FACT). The new drives arrive UNDER mp8 (bound_under_parent), so no new mp line.
|
||||
- (3) A plain-host-path bind is not classified (classifyRoot knows only ${..._PATH}), so it is in no app capture set
|
||||
at any tier. An unlisted writable ${USERDATA_PATH} bind WOULD be mandatory (Tier 3 included).
|
||||
@@ -0,0 +1,37 @@
|
||||
bookstack Up 5 minutes (healthy)
|
||||
bookstack-db Up 5 minutes (healthy)
|
||||
cloudflared Up 5 minutes (healthy)
|
||||
felhom-controller Up 5 minutes (healthy)
|
||||
filebrowser Up 5 minutes (healthy)
|
||||
paperless-postgres Up 5 minutes (healthy)
|
||||
paperless-redis Up 5 minutes (healthy)
|
||||
paperless-webserver Up 5 minutes (healthy)
|
||||
privatebin Up 5 minutes (healthy)
|
||||
qbittorrent Up 4 minutes (healthy)
|
||||
radarr Up 4 minutes (healthy)
|
||||
sonarr Up 4 minutes (healthy)
|
||||
traefik Up 5 minutes
|
||||
transmission Up 4 minutes (healthy)
|
||||
TARGET SOURCE
|
||||
/mnt/felhom-drives /dev/mapper/pve-root[/mnt/felhom-drives]
|
||||
|-/mnt/felhom-drives/media_a /dev/sdc[/felhom-data]
|
||||
|-/mnt/felhom-drives/adatlemez /dev/sdb[/felhom-data]
|
||||
`-/mnt/felhom-drives/media_b /dev/sdd[/felhom-data]
|
||||
sonarr sees: 14 14
|
||||
radarr sees: 14 14
|
||||
qbittorrent sees: 14 14
|
||||
transmission sees: 14 14
|
||||
24: repo_url: file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
2026/10/14 15:28:40 [INFO] [stacks] Stack paperless-ngx started successfully (took 11.8s)
|
||||
2026/10/14 15:28:40 [INFO] [gate] boot 1791991571-10166: live bind confirmed — recreating drive-backed app radarr (state=starting) onto /mnt/felhom-drives/media_a
|
||||
2026/10/14 15:28:44 [INFO] [stacks] Stack paperless-ngx post-start status:
|
||||
2026/10/14 15:28:44 [INFO] [stacks] paperless-postgres postgres:18-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873 running Up 14 seconds (healthy)
|
||||
2026/10/14 15:28:44 [INFO] [stacks] paperless-redis redis:7-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499 running Up 14 seconds (healthy)
|
||||
2026/10/14 15:28:44 [INFO] [stacks] paperless-webserver ghcr.io/paperless-ngx/paperless-ngx:2.20.15@sha256:6c86cad803970ea782683a8e80e7403444c5bf3cf70de63b4d3c8e87500db92f running Up 4 seconds (health: starting)
|
||||
2026/10/14 15:28:46 [INFO] [gate] boot 1791991571-10166: live bind confirmed — recreating drive-backed app sonarr (state=starting) onto /mnt/felhom-drives/media_a
|
||||
2026/10/14 15:28:51 [INFO] [gate] boot 1791991571-10166: live bind confirmed — recreating drive-backed app transmission (state=running) onto /mnt/felhom-drives/media_a
|
||||
2026/10/14 15:28:58 [INFO] [gate] boot 1791991571-10166: live bind confirmed — recreating drive-backed app qbittorrent (state=running) onto /mnt/felhom-drives/media_a
|
||||
2026/10/14 15:29:01 [INFO] [gate] boot 1791991571-10166: re-syncing FileBrowser mounts against the live binds
|
||||
2026/10/14 15:29:01 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/paperless-ngx/docker-compose.yml
|
||||
2026/10/14 15:33:14 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/paperless-ngx/docker-compose.yml
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mirror main 0ad6383 DRILL media-all-drives: six media templates bind every test drive's userdata/media at the same path (Tester 1, 2026-10-14)
|
||||
|
||||
drill repo main: 0ad6383
|
||||
@@ -0,0 +1,22 @@
|
||||
24: repo_url: file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
24c24
|
||||
< repo_url: https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
|
||||
---
|
||||
> repo_url: file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated radarr/.felhom.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated sonarr/docker-compose.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated sonarr/.felhom.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated transmission/docker-compose.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated transmission/.felhom.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] catalog moved 8ee687e544e0 -> 0ad638315272 (0 new, 6 stack dir(s) changed)
|
||||
2026/10/14 14:04:37 [INFO] [sync] Catalog sync complete
|
||||
2026/10/14 14:04:37 [INFO] [sync] Initial sync: Sablonok frissítve — frissítve: jellyfin, plex, qbittorrent, radarr, sonarr, transmission
|
||||
|
||||
24: repo_url: file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
25: sync_interval: 15m
|
||||
-rw------- 1 root root 3044 Oct 4 19:41 /var/lib/docker/volumes/felhom-controller-data/_data/controller.yaml.pre-media-2026-10-14
|
||||
mirror main 0ad6383 DRILL media-all-drives: six media templates bind every test drive's userdata/media at the same path (Tester 1, 2026-10-14)
|
||||
cache HEAD 0ad6383 DRILL media-all-drives: six media templates bind every test drive's userdata/media at the same path (Tester 1, 2026-10-14)
|
||||
file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
2026-10-14T16:05:05+02:00
|
||||
== control 1: Tester 1 cache carries the drill layout
|
||||
perl: warning: Setting locale failed.
|
||||
perl: warning: Please check that your locale settings:
|
||||
LANGUAGE = (unset),
|
||||
LC_ALL = (unset),
|
||||
LC_CTYPE = "UTF-8",
|
||||
LC_NUMERIC = (unset),
|
||||
LC_COLLATE = (unset),
|
||||
LC_TIME = (unset),
|
||||
LC_MESSAGES = (unset),
|
||||
LC_MONETARY = (unset),
|
||||
LC_ADDRESS = (unset),
|
||||
LC_IDENTIFICATION = (unset),
|
||||
LC_MEASUREMENT = (unset),
|
||||
LC_PAPER = (unset),
|
||||
LC_TELEPHONE = (unset),
|
||||
LC_NAME = (unset),
|
||||
LANG = "en_US.UTF-8"
|
||||
are supported and installed on your system.
|
||||
perl: warning: Falling back to a fallback locale ("en_US.UTF-8").
|
||||
1
|
||||
0ad6383
|
||||
== control 2: the other boxes' caches still follow the live catalog
|
||||
-- demo-hp 9201:
|
||||
8ee687e
|
||||
https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
|
||||
0
|
||||
-- demo-hp 9202:
|
||||
8ee687e
|
||||
https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
|
||||
0
|
||||
-- N100 (felhom-pve) guest:
|
||||
LANGUAGE = (unset),
|
||||
LANG = "en_US.UTF-8"
|
||||
guest 9201
|
||||
LANGUAGE = (unset),
|
||||
LANG = "en_US.UTF-8"
|
||||
8ee687e
|
||||
https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
|
||||
0
|
||||
== control 3: live catalog main
|
||||
8ee687e544e0bc52980458ebf3c86e9484dba1fa refs/heads/main
|
||||
@@ -0,0 +1,38 @@
|
||||
16:05:42 qbittorrent: POST deploy {"DOMAIN": "enkicsifelhom.hu", "SUBDOMAIN": "torrent", "HDD_PATH": "/mnt/felhom-drives/media_a", "QBT_ADMIN_PASSWORD": "<generated, kept in secrets.json>"} -> 202 {"ok": true, "message": "Telepítés elindítva – az állapot a kártyán követhető"}
|
||||
16:05:57 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/media_a/userdata/media->/mnt/felhom-drives/media_a/userdata/media:true /mnt/felhom-drives/media_b/userdata/media->/mnt/felhom-drives/media_b/userdata/media:true
|
||||
2026/10/14 14:05:42 [INFO] [api] Deploy requested for stack: qbittorrent
|
||||
2026/10/14 14:05:42 [INFO] [stacks] Deploying stack qbittorrent with 4 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH, QBT_ADMIN_PASSWORD]
|
||||
2026/10/14 14:05:42 [INFO] Event pushed: app_deploy_started (info) [hu-only] — Alkalmazás telepítése elindult: qBittorrent
|
||||
2026/10/14 14:05:47 [INFO] [stacks] Stack qbittorrent deployed successfully (took 5.5s)
|
||||
2026/10/14 14:05:48 [INFO] Event pushed: app_deployed (info) [hu-only] — Alkalmazás telepítve: qBittorrent
|
||||
2026/10/14 14:05:53 [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 14:05:53 [INFO] [stacks] qbittorrent: install hold OPENED by after_install — the app is reached as without a hold
|
||||
|
||||
16:06:01 transmission: POST deploy {"DOMAIN": "enkicsifelhom.hu", "SUBDOMAIN": "transmission", "HDD_PATH": "/mnt/felhom-drives/media_a", "TR_ADMIN_PASSWORD": "<generated, kept in secrets.json>"} -> 202 {"ok": true, "message": "Telepítés elindítva – az állapot a kártyán követhető"}
|
||||
16:06:21 transmission: deployed, state=running, pinned={'transmission': 'lscr.io/linuxserver/transmission:version-4.1.3-r0'}
|
||||
running /var/lib/docker/volumes/transmission_transmission_config/_data->/config:true /mnt/felhom-drives/media_a/userdata/media/transmission/watch->/watch:true /mnt/felhom-drives/media_a/userdata/media->/mnt/felhom-drives/media_a/userdata/media:true /mnt/felhom-drives/media_b/userdata/media->/mnt/felhom-drives/media_b/userdata/media:true
|
||||
2026/10/14 14:06:01 [INFO] [api] Deploy requested for stack: transmission
|
||||
2026/10/14 14:06:01 [INFO] [stacks] Deploying stack transmission with 4 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH, TR_ADMIN_PASSWORD]
|
||||
2026/10/14 14:06:01 [INFO] Event pushed: app_deploy_started (info) [hu-only] — Alkalmazás telepítése elindult: Transmission
|
||||
2026/10/14 14:06:08 [INFO] [stacks] Stack transmission deployed successfully (took 6.7s)
|
||||
2026/10/14 14:06:08 [INFO] Event pushed: app_deployed (info) [hu-only] — Alkalmazás telepítve: Transmission
|
||||
|
||||
16:06:26 sonarr: POST deploy {"DOMAIN": "enkicsifelhom.hu", "SUBDOMAIN": "sonarr", "HDD_PATH": "/mnt/felhom-drives/media_a"} -> 202 {"ok": true, "data": {"warning": "Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát. Normál használat mellett ez nem okoz problémát."}, "message": "Telepítés elindítva – az ál
|
||||
16:07:01 sonarr: deployed, state=running, pinned={'sonarr': 'lscr.io/linuxserver/sonarr:4.0.20'}
|
||||
running /var/lib/docker/volumes/sonarr_sonarr_config/_data->/config:true /mnt/felhom-drives/media_a/userdata/media->/mnt/felhom-drives/media_a/userdata/media:true /mnt/felhom-drives/media_b/userdata/media->/mnt/felhom-drives/media_b/userdata/media:true
|
||||
2026/10/14 14:06:26 [INFO] [api] Deploy requested for stack: sonarr
|
||||
2026/10/14 14:06:26 [INFO] [stacks] Deploying stack sonarr with 3 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH]
|
||||
2026/10/14 14:06:26 [INFO] Event pushed: app_deploy_started (info) [hu-only] — Alkalmazás telepítése elindult: Sonarr
|
||||
2026/10/14 14:06:38 [INFO] [stacks] Stack sonarr deployed successfully (took 12.3s)
|
||||
2026/10/14 14:06:39 [INFO] Event pushed: app_deployed (info) [hu-only] — Alkalmazás telepítve: Sonarr
|
||||
|
||||
16:07:05 radarr: POST deploy {"DOMAIN": "enkicsifelhom.hu", "SUBDOMAIN": "radarr", "HDD_PATH": "/mnt/felhom-drives/media_a"} -> 202 {"ok": true, "data": {"warning": "Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát. Normál használat mellett ez nem okoz problémát."}, "message": "Telepítés elindítva – az ál
|
||||
16:07: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/media_a/userdata/media->/mnt/felhom-drives/media_a/userdata/media:true /mnt/felhom-drives/media_b/userdata/media->/mnt/felhom-drives/media_b/userdata/media:true
|
||||
2026/10/14 14:07:05 [INFO] [api] Deploy requested for stack: radarr
|
||||
2026/10/14 14:07:05 [INFO] [stacks] Deploying stack radarr with 3 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH]
|
||||
2026/10/14 14:07:05 [INFO] Event pushed: app_deploy_started (info) [hu-only] — Alkalmazás telepítése elindult: Radarr
|
||||
2026/10/14 14:07:12 [INFO] [stacks] Stack radarr deployed successfully (took 6.6s)
|
||||
2026/10/14 14:07:12 [INFO] Event pushed: app_deployed (info) [hu-only] — Alkalmazás telepítve: Radarr
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
sonarr: env SONARR__AUTH__APIKEY=<set> -> status with that key HTTP 200; with a wrong key HTTP 401; key written to config.xml: 0
|
||||
radarr: env RADARR__AUTH__APIKEY=<set> -> status with that key HTTP 200; with a wrong key HTTP 401; key written to config.xml: 0
|
||||
0
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
== qBittorrent http://172.18.0.8:8080 (password: the deploy secret, read from the container's environment)
|
||||
POST http://172.18.0.8:8080/api/v2/auth/login -> 204 ''
|
||||
session cookie name: QBT_SID_8080
|
||||
GET http://172.18.0.8:8080/api/v2/app/preferences -> 200 {add_stopped_enabled, add_to_top_of_queue, add_trackers, add_trackers_enabled, add_trackers_from_url_enabled, add_trackers_url, add_trackers_url_list, alt_dl_limit, alt_up_limit, alternative_webui_enabled, alternative_webui_path, announce_ip, announce_port, announce_to_all_tiers, announce_to_all_trac}
|
||||
before: auto_tmm_enabled=False save_path=/mnt/felhom-drives/media_a/userdata/media/torrents temp_path_enabled=True temp_path=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
POST http://172.18.0.8:8080/api/v2/app/setPreferences -> 200 ''
|
||||
GET http://172.18.0.8:8080/api/v2/app/preferences -> 200 {add_stopped_enabled, add_to_top_of_queue, add_trackers, add_trackers_enabled, add_trackers_from_url_enabled, add_trackers_url, add_trackers_url_list, alt_dl_limit, alt_up_limit, alternative_webui_enabled, alternative_webui_path, announce_ip, announce_port, announce_to_all_tiers, announce_to_all_trac}
|
||||
after : auto_tmm_enabled=True (a category's save path applies only in automatic mode)
|
||||
GET http://172.18.0.8:8080/api/v2/torrents/categories -> 200 {}
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/createCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/createCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/createCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/createCategory -> 200 ''
|
||||
GET http://172.18.0.8:8080/api/v2/torrents/categories -> 200 {movies-drive-a, movies-drive-b, tv-drive-a, tv-drive-b}
|
||||
category movies-drive-a: savePath=/mnt/felhom-drives/media_a/userdata/media/torrents/movies downloadPath=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
category movies-drive-b: savePath=/mnt/felhom-drives/media_b/userdata/media/torrents/movies downloadPath=/mnt/felhom-drives/media_b/userdata/media/torrents/.temp
|
||||
category tv-drive-a: savePath=/mnt/felhom-drives/media_a/userdata/media/torrents/tv downloadPath=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
category tv-drive-b: savePath=/mnt/felhom-drives/media_b/userdata/media/torrents/tv downloadPath=/mnt/felhom-drives/media_b/userdata/media/torrents/.temp
|
||||
== Transmission http://172.18.0.9:9091/transmission/rpc (password: the deploy secret, read from the container's environment)
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
before: {"download-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/complete", "incomplete-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/incomplete", "incomplete-dir-enabled": true, "rename-partial-files": true}
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
after : {"download-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/complete", "incomplete-dir-enabled": false, "rename-partial-files": true} (one incomplete folder for all torrents would be on drive A)
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
probe Host: transmission:9091 -> HTTP 200 {'arguments': {'version': '4.1.3 (838877323f)'}, 'result': 'success'}
|
||||
== sonarr http://172.18.0.10:8989/api/v3 (key: <ApiKey> in /config/config.xml, read from the config volume)
|
||||
GET http://172.18.0.10:8989/api/v3/tag -> 200 [list of 0]
|
||||
POST http://172.18.0.10:8989/api/v3/tag -> 201 {id, label}
|
||||
POST http://172.18.0.10:8989/api/v3/tag -> 201 {id, label}
|
||||
tags: {'drive-a': 1, 'drive-b': 2}
|
||||
GET http://172.18.0.10:8989/api/v3/rootfolder -> 200 [list of 0]
|
||||
POST http://172.18.0.10:8989/api/v3/rootfolder -> 201 {accessible, freeSpace, id, path, unmappedFolders}
|
||||
POST http://172.18.0.10:8989/api/v3/rootfolder -> 201 {accessible, freeSpace, id, path, unmappedFolders}
|
||||
GET http://172.18.0.10:8989/api/v3/rootfolder -> 200 [list of 2]
|
||||
root folder: /mnt/felhom-drives/media_a/userdata/media/tv accessible=True freeSpace=99701432320
|
||||
root folder: /mnt/felhom-drives/media_b/userdata/media/tv accessible=True freeSpace=99701567488
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient/schema -> 200 [list of 18]
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient -> 200 [list of 0]
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 400 [list of 1]
|
||||
client Transmission drive-a: REFUSED by sonarr's own connection test: [{"isWarning": false, "propertyName": "TvCategory", "errorMessage": "Cannot use Category and Directory", "attemptedValue": "tv-sonarr", "severity": "error"}]
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 400 [list of 1]
|
||||
client Transmission drive-b: REFUSED by sonarr's own connection test: [{"isWarning": false, "propertyName": "TvCategory", "errorMessage": "Cannot use Category and Directory", "attemptedValue": "tv-sonarr", "severity": "error"}]
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient -> 200 [list of 2]
|
||||
client qBittorrent drive-a: impl=QBittorrent enable=True tags=[1] host=qbittorrent category=tv-drive-a directory=None
|
||||
client qBittorrent drive-b: impl=QBittorrent enable=True tags=[2] host=qbittorrent category=tv-drive-b directory=None
|
||||
GET http://172.18.0.10:8989/api/v3/config/mediamanagement -> 200 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
before: minimumFreeSpaceWhenImporting=100 MB, copyUsingHardlinks=True
|
||||
PUT http://172.18.0.10:8989/api/v3/config/mediamanagement/1 -> 202 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
GET http://172.18.0.10:8989/api/v3/config/mediamanagement -> 200 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
after : minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True (default kept)
|
||||
== radarr http://172.18.0.11:7878/api/v3 (key: <ApiKey> in /config/config.xml, read from the config volume)
|
||||
GET http://172.18.0.11:7878/api/v3/tag -> 200 [list of 0]
|
||||
POST http://172.18.0.11:7878/api/v3/tag -> 201 {id, label}
|
||||
POST http://172.18.0.11:7878/api/v3/tag -> 201 {id, label}
|
||||
tags: {'drive-a': 1, 'drive-b': 2}
|
||||
GET http://172.18.0.11:7878/api/v3/rootfolder -> 200 [list of 0]
|
||||
POST http://172.18.0.11:7878/api/v3/rootfolder -> 201 {accessible, freeSpace, id, path, unmappedFolders}
|
||||
POST http://172.18.0.11:7878/api/v3/rootfolder -> 201 {accessible, freeSpace, id, path, unmappedFolders}
|
||||
GET http://172.18.0.11:7878/api/v3/rootfolder -> 200 [list of 2]
|
||||
root folder: /mnt/felhom-drives/media_a/userdata/media/movies accessible=True freeSpace=99701432320
|
||||
root folder: /mnt/felhom-drives/media_b/userdata/media/movies accessible=True freeSpace=99701567488
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient/schema -> 200 [list of 19]
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient -> 200 [list of 0]
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 400 [list of 1]
|
||||
client Transmission drive-a: REFUSED by radarr's own connection test: [{"isWarning": false, "propertyName": "MovieCategory", "errorMessage": "Cannot use Category and Directory", "attemptedValue": "radarr", "severity": "error"}]
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 400 [list of 1]
|
||||
client Transmission drive-b: REFUSED by radarr's own connection test: [{"isWarning": false, "propertyName": "MovieCategory", "errorMessage": "Cannot use Category and Directory", "attemptedValue": "radarr", "severity": "error"}]
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient -> 200 [list of 2]
|
||||
client qBittorrent drive-a: impl=QBittorrent enable=True tags=[1] host=qbittorrent category=movies-drive-a directory=None
|
||||
client qBittorrent drive-b: impl=QBittorrent enable=True tags=[2] host=qbittorrent category=movies-drive-b directory=None
|
||||
GET http://172.18.0.11:7878/api/v3/config/mediamanagement -> 200 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
before: minimumFreeSpaceWhenImporting=100 MB, copyUsingHardlinks=True
|
||||
PUT http://172.18.0.11:7878/api/v3/config/mediamanagement/1 -> 202 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
GET http://172.18.0.11:7878/api/v3/config/mediamanagement -> 200 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
after : minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True (default kept)
|
||||
== done; Transmission reachable by its container name: True
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
== qBittorrent http://172.18.0.8:8080 (password: the deploy secret, read from the container's environment)
|
||||
POST http://172.18.0.8:8080/api/v2/auth/login -> 204 ''
|
||||
session cookie name: QBT_SID_8080
|
||||
GET http://172.18.0.8:8080/api/v2/app/preferences -> 200 {add_stopped_enabled, add_to_top_of_queue, add_trackers, add_trackers_enabled, add_trackers_from_url_enabled, add_trackers_url, add_trackers_url_list, alt_dl_limit, alt_up_limit, alternative_webui_enabled, alternative_webui_path, announce_ip, announce_port, announce_to_all_tiers, announce_to_all_trac}
|
||||
before: auto_tmm_enabled=True save_path=/mnt/felhom-drives/media_a/userdata/media/torrents temp_path_enabled=True temp_path=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
POST http://172.18.0.8:8080/api/v2/app/setPreferences -> 200 ''
|
||||
GET http://172.18.0.8:8080/api/v2/app/preferences -> 200 {add_stopped_enabled, add_to_top_of_queue, add_trackers, add_trackers_enabled, add_trackers_from_url_enabled, add_trackers_url, add_trackers_url_list, alt_dl_limit, alt_up_limit, alternative_webui_enabled, alternative_webui_path, announce_ip, announce_port, announce_to_all_tiers, announce_to_all_trac}
|
||||
after : auto_tmm_enabled=True (a category's save path applies only in automatic mode)
|
||||
GET http://172.18.0.8:8080/api/v2/torrents/categories -> 200 {movies-drive-a, movies-drive-b, tv-drive-a, tv-drive-b}
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/editCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/editCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/editCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/editCategory -> 200 ''
|
||||
GET http://172.18.0.8:8080/api/v2/torrents/categories -> 200 {movies-drive-a, movies-drive-b, tv-drive-a, tv-drive-b}
|
||||
category movies-drive-a: savePath=/mnt/felhom-drives/media_a/userdata/media/torrents/movies downloadPath=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
category movies-drive-b: savePath=/mnt/felhom-drives/media_b/userdata/media/torrents/movies downloadPath=/mnt/felhom-drives/media_b/userdata/media/torrents/.temp
|
||||
category tv-drive-a: savePath=/mnt/felhom-drives/media_a/userdata/media/torrents/tv downloadPath=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
category tv-drive-b: savePath=/mnt/felhom-drives/media_b/userdata/media/torrents/tv downloadPath=/mnt/felhom-drives/media_b/userdata/media/torrents/.temp
|
||||
== Transmission http://172.18.0.9:9091/transmission/rpc (password: the deploy secret, read from the container's environment)
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
before: {"download-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/complete", "incomplete-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/incomplete", "incomplete-dir-enabled": false, "rename-partial-files": true}
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
after : {"download-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/complete", "incomplete-dir-enabled": false, "rename-partial-files": true} (one incomplete folder for all torrents would be on drive A)
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
probe Host: transmission:9091 -> HTTP 200 {'arguments': {'version': '4.1.3 (838877323f)'}, 'result': 'success'}
|
||||
== sonarr http://172.18.0.10:8989/api/v3 (key: <ApiKey> in /config/config.xml, read from the config volume)
|
||||
GET http://172.18.0.10:8989/api/v3/tag -> 200 [list of 2]
|
||||
tags: {'drive-a': 1, 'drive-b': 2}
|
||||
GET http://172.18.0.10:8989/api/v3/rootfolder -> 200 [list of 2]
|
||||
GET http://172.18.0.10:8989/api/v3/rootfolder -> 200 [list of 2]
|
||||
root folder: /mnt/felhom-drives/media_a/userdata/media/tv accessible=True freeSpace=99701432320
|
||||
root folder: /mnt/felhom-drives/media_b/userdata/media/tv accessible=True freeSpace=99701567488
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient/schema -> 200 [list of 18]
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient -> 200 [list of 2]
|
||||
client qBittorrent drive-a: exists — left as it is
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
client qBittorrent drive-b: exists — left as it is
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient -> 200 [list of 4]
|
||||
client qBittorrent drive-a: impl=QBittorrent enable=True tags=[1] host=qbittorrent category=tv-drive-a directory=None
|
||||
client qBittorrent drive-b: impl=QBittorrent enable=True tags=[2] host=qbittorrent category=tv-drive-b directory=None
|
||||
client Transmission drive-a: impl=Transmission enable=True tags=[1] host=transmission category= directory=/mnt/felhom-drives/media_a/userdata/media/transmission/tv
|
||||
client Transmission drive-b: impl=Transmission enable=True tags=[2] host=transmission category= directory=/mnt/felhom-drives/media_b/userdata/media/transmission/tv
|
||||
GET http://172.18.0.10:8989/api/v3/config/mediamanagement -> 200 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
before: minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True
|
||||
PUT http://172.18.0.10:8989/api/v3/config/mediamanagement/1 -> 202 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
GET http://172.18.0.10:8989/api/v3/config/mediamanagement -> 200 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
after : minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True (default kept)
|
||||
== radarr http://172.18.0.11:7878/api/v3 (key: <ApiKey> in /config/config.xml, read from the config volume)
|
||||
GET http://172.18.0.11:7878/api/v3/tag -> 200 [list of 2]
|
||||
tags: {'drive-a': 1, 'drive-b': 2}
|
||||
GET http://172.18.0.11:7878/api/v3/rootfolder -> 200 [list of 2]
|
||||
GET http://172.18.0.11:7878/api/v3/rootfolder -> 200 [list of 2]
|
||||
root folder: /mnt/felhom-drives/media_a/userdata/media/movies accessible=True freeSpace=99701432320
|
||||
root folder: /mnt/felhom-drives/media_b/userdata/media/movies accessible=True freeSpace=99701567488
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient/schema -> 200 [list of 19]
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient -> 200 [list of 2]
|
||||
client qBittorrent drive-a: exists — left as it is
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
client qBittorrent drive-b: exists — left as it is
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient -> 200 [list of 4]
|
||||
client qBittorrent drive-a: impl=QBittorrent enable=True tags=[1] host=qbittorrent category=movies-drive-a directory=None
|
||||
client qBittorrent drive-b: impl=QBittorrent enable=True tags=[2] host=qbittorrent category=movies-drive-b directory=None
|
||||
client Transmission drive-a: impl=Transmission enable=True tags=[1] host=transmission category= directory=/mnt/felhom-drives/media_a/userdata/media/transmission/movies
|
||||
client Transmission drive-b: impl=Transmission enable=True tags=[2] host=transmission category= directory=/mnt/felhom-drives/media_b/userdata/media/transmission/movies
|
||||
GET http://172.18.0.11:7878/api/v3/config/mediamanagement -> 200 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
before: minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True
|
||||
PUT http://172.18.0.11:7878/api/v3/config/mediamanagement/1 -> 202 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
GET http://172.18.0.11:7878/api/v3/config/mediamanagement -> 200 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
after : minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True (default kept)
|
||||
== done; Transmission reachable by its container name: True
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Media apps on every drive — the rig on the Tester 1 box (2026-10-14)
|
||||
|
||||
Task: `TASK-media-all-drives` (operator rulings 2026-10-14, `09` §3). Box: Tester 1 (VM 341 on demo-hp, guest 9201,
|
||||
`enkicsifelhom.hu`), controller 0.310.0, agent 0.156.1. Design written from this: `architecture/15-media-library.md`.
|
||||
Predictions were written before each measurement: `A-drives/PREDICTIONS-A4-A6.md`, `S-PREDICTIONS.md`.
|
||||
|
||||
| Part | Result | Evidence |
|
||||
|---|---|---|
|
||||
| A1 | Tester 1 read first: one drive `adatlemez` (default; not the backup target — that is `local`), apps bookstack, paperless-ngx, privatebin; guest 4096 MB (max 5824), host VM 8 GB; off-site on; demo-hp `/mnt/hdd_1` 764 G free of 938 G | `A-drives/A1-*.txt` |
|
||||
| A2 | Two 100 GB disks hot-added as `scsi2`/`scsi3`, `backup=0`, on `nvme-scratch` (`/mnt/hdd_1` root). Worst case (every sparse image full) leaves ≥ 391 G free = 42 %. Memory NOT raised: the six media apps' summed peaks are 1199 MB (`sizing.json`); with the box's three apps and the platform ~2.7 GB < 4096 MB | `A-drives/A2-disks-added.txt` |
|
||||
| A4 | Both enrolled through `POST /api/storage/init` (the wizard's call), ~2 s each: `media_a` (sdc), `media_b` (sdd). Neither became the default (sent `set_default:false`; **the wizard's checkbox is ON by default**) nor the backup target (`target: local` before and after) | `A-drives/A4-*.txt` |
|
||||
| A5 | Skeleton on both (`userdata/{documents,downloads,media/{movies,tv,torrents,transmission,photos,books,…},roms}`); FileBrowser recreated with both drives (`/srv/media_a`, `/srv/media_b`). The dashboard: „Aktív", „Nincs alkalmazás ezen a tárolón" — **nothing about apps that would need a restart** | `A-drives/A5-*.txt` |
|
||||
| A6 | (1) demo-hp has no backup job at all; `backup=0` read back. (2) Tester 1's whole-guest backups — local AND off-site PBS — log „excluding bind mount point mp8 ('/mnt/felhom-drives')". (3) The controller's own classifier over the test templates: no media bind in any capture set (control: the live templates DO capture — and a decoy with unlisted writable binds is captured mandatory) | `A-drives/A6-*.txt` |
|
||||
| B1 | Tester 1 points at a **local mirror** of the drill catalog (`file:///opt/docker/felhom-controller/drill-catalog.git`) — no credential on the box (R-992: a read-only token could not be minted, the 403 is quoted in `tools/repoint.py`). Three controls pass: Tester 1's cache = drill `0ad6383`; demo-hp 9201, 9202 and the N100 guest = live `8ee687e`; live `main` unchanged | `B1-*.txt` |
|
||||
| B2 | Drill commit `0ad6383`: six templates bind `/mnt/felhom-drives/{media_a,media_b}/userdata/media` at the same path (`:ro` for Plex/Jellyfin); images unchanged | `tools/make_drill.py` |
|
||||
| B4 | qBittorrent, Transmission, Sonarr, Radarr deployed through `POST /api/stacks/<app>/deploy`; plain host paths accepted, no warning | `B4-deploy.txt` |
|
||||
| C | `tools/wire.py`: per drive a qBittorrent category, Sonarr/Radarr root folder + tag + two client entries; min free space 10 GB; hardlinks on (default) | `C-wire-run*.txt` |
|
||||
| C | API key: read from `/config/config.xml`; it CAN be set at install: `SONARR__AUTH__APIKEY` / `RADARR__AUTH__APIKEY` env → 200 with that key, 401 with another | `C-apikey-at-install.txt` |
|
||||
| S1 | Today's two-bind shape: `ln: … Cross-device link` (EXDEV) on one filesystem → copy (2 inodes). One bind per drive: link count 2, one inode | `S1-hardlink.txt` |
|
||||
| S2 | One public-domain film (archive.org, CC PD Mark) on drive B: through Transmission and through qBittorrent → **hardlink** in `media_b/…/movies`. archive.org's web seed failed the last piece for qBittorrent; a local peer (the same torrent in Transmission) finished it — recorded | `S2-*.txt` |
|
||||
| S3 | Tag drive-a, no client forced → „Transmission drive-a", hardlink on A. Mismatch (root B, tag A) → download on A, **copy** on B (link count 1 each) | `S3-*.txt` |
|
||||
| — | **17:25 CEST: demo-hp was power-cut by another session's Immich drill** (`plugcut.py --plug hp`) — Tester 1 went down with it and came back by itself; the boot gate recreated the four media apps onto drive A; all test films then removed (0 files left) | `AFTER-power-cut-1725.txt` |
|
||||
@@ -0,0 +1,14 @@
|
||||
# Predictions — written before S2/S3 ran (2026-10-14 ~16:30 CEST)
|
||||
|
||||
S2 (root B, tag drive-b; once through qBittorrent drive-b, once through Transmission drive-b)
|
||||
- Radarr approves the pushed release, sends it to the one enabled drive-b client.
|
||||
- qBittorrent: the torrent lands in media_b/userdata/media/torrents/movies/<item>/ (category save path, automatic mode);
|
||||
incomplete pieces in media_b/.../torrents/.temp. Transmission: in media_b/.../transmission/movies/<item>/.
|
||||
- Radarr imports by HARDLINK into media_b/userdata/media/movies/Night of the Living Dead (1968)/: link count 2, same inode.
|
||||
No remote path mapping is needed, because every container sees the same path.
|
||||
|
||||
S3
|
||||
- tag routing: root A + tag drive-a, no client forced → one of the drive-a clients takes it (never a drive-b client);
|
||||
download on A, hardlink in A's movies.
|
||||
- the mismatch: root B + tag drive-a → download on A, import into B as a COPY (link count 1, two inodes); the A file
|
||||
keeps seeding. Radarr does not refuse it.
|
||||
@@ -0,0 +1,30 @@
|
||||
### PREDICTION: old shape (two binds) -> ln fails EXDEV, mv falls back to copy (link count 1, two inodes); new shape (one bind per drive) -> hardlink, link count 2, same inode
|
||||
### OLD SHAPE (live templates): -v A/media/movies:/media/movies -v A/downloads:/downloads
|
||||
ln-exit=1
|
||||
copied
|
||||
/downloads/s1-probe/film.mkv links=1 inode=4194328 dev=2080
|
||||
/media/movies/s1-probe/film.mkv links=1 inode=4194329 dev=2080
|
||||
### guest view after OLD shape:
|
||||
/mnt/felhom-drives/media_a/userdata/downloads/s1-probe/film.mkv links=1 inode=4194328
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies/s1-probe/film.mkv links=1 inode=4194329
|
||||
### NEW SHAPE: -v A/media:A/media (same path)
|
||||
ln-exit=0
|
||||
/mnt/felhom-drives/media_a/userdata/media/torrents/s1-probe/film.mkv links=2 inode=4194328 dev=2080
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies/s1-probe/film.mkv links=2 inode=4194328 dev=2080
|
||||
### guest view after NEW shape:
|
||||
/mnt/felhom-drives/media_a/userdata/media/torrents/s1-probe/film.mkv links=2 inode=4194328
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies/s1-probe/film.mkv links=2 inode=4194328
|
||||
### cleanup
|
||||
/mnt/felhom-drives/media_a/userdata/downloads:
|
||||
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies:
|
||||
|
||||
/mnt/felhom-drives/media_a/userdata/media/torrents:
|
||||
|
||||
ln: failed to create hard link '/media/movies/f' => '/downloads/s1-probe/f': Cross-device link
|
||||
mv-exit=0
|
||||
/media/movies/f-moved links=1 inode=4194328
|
||||
/mnt/felhom-drives/media_a/userdata/downloads:
|
||||
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies:
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# ATTEMPT 1 (16:13–16:27 CEST) — stopped by CC. Radarr approved and grabbed (qBittorrent drive-b took it: category
|
||||
# movies-drive-b, save path media_b/.../torrents/movies, incomplete path media_b/.../torrents/.temp — read from
|
||||
# qBittorrent's own torrents/info). The download stalled at 6.09 % (32 MiB): 0 peers, and archive.org's web seed
|
||||
# answered "500 Internal Server Error" (qBittorrent log, URL https://dn721601.ca.archive.org/); stop/start did not
|
||||
# revive it. A plain curl range request to the same file from the same container answered 206 at the same time.
|
||||
# So the source, not the chain, stalled. The run was stopped and retried (below).
|
||||
# ATTEMPT 2 (16:38–17:09 CEST), item night-of-the-living-dead-1968_202508:
|
||||
=== s2-qbit: root=/mnt/felhom-drives/media_b/userdata/media/movies tag=drive-b client=qBittorrent drive-b item=archive.org/night-of-the-living-dead-1968_202508
|
||||
clients: [('qBittorrent drive-a', True, [1]), ('qBittorrent drive-b', True, [2]), ('Transmission drive-a', True, [1]), ('Transmission drive-b', False, [2])]
|
||||
movie added: id=3 'Night of the Living Dead' (1968) path=/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968) tags=[2] profile=Any
|
||||
release push -> 200 approved=True rejections=[] downloadAllowed=True
|
||||
+11s grabbed: downloadClient=qBittorrent downloadClientName=qBittorrent drive-b downloadId=28766E9244F6920800B29298E9BBF00E41185CBA
|
||||
+61s queue: status=warning trackedState=downloading left=596675329 client=qBittorrent drive-b output=None msgs=[]
|
||||
+121s queue: status=downloading trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+181s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+241s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+302s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+362s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+422s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+482s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+542s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+602s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+662s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+722s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+782s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+842s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+902s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+962s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1022s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1082s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1142s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1202s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1262s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1322s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1382s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1442s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1502s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1562s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1622s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1682s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1742s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1802s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1862s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1872s history downloadFolderImported: {"downloadClient": "qBittorrent", "downloadClientName": "qBittorrent drive-b", "droppedPath": "/mnt/felhom-drives/media_b/userdata/media/torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4", "importedPath": "/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4", "size": "596645320"}
|
||||
download file: /mnt/felhom-drives/media_b/userdata/media/torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4 links=2 inode=131097 size=596645320
|
||||
library file : /mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 links=2 inode=131097 size=596645320
|
||||
RESULT s2-qbit: HARDLINK (same inode); download on media_b, library on media_b
|
||||
[finished]
|
||||
|
||||
# NOTE on attempt 2: qBittorrent reached 99.994 % at ~16:41 (7.7 MB/s from archive.org's web seed) and then stalled on
|
||||
# the LAST piece (the film's tail + archive.org's three side files: __ia_thumb.jpg, _meta.sqlite, _meta.xml). Its log
|
||||
# shows "Received error message from URL seed ... dn721505.ca.archive.org ... 500 Internal Server Error"; 8 stop/start
|
||||
# retries over 6 min did not help, while curl range requests for the same three files from DooPlex answered 206. To
|
||||
# finish, CC added the SAME public-domain torrent to Transmission (download dir media_b/.../transmission/s2-helper) and
|
||||
# gave qBittorrent Transmission's container address as a peer (POST /api/v2/torrents/addPeers 172.18.0.9:51413) at
|
||||
# 17:03; qBittorrent completed within 40 s and Radarr imported. The chain itself (grab by client, category save path,
|
||||
# move from .temp, hardlink import) is unaffected; the source's web seed was the fault.
|
||||
@@ -0,0 +1,16 @@
|
||||
=== s2-trans: root=/mnt/felhom-drives/media_b/userdata/media/movies tag=drive-b client=Transmission drive-b item=archive.org/night-of-the-living-dead-1968_202508
|
||||
clients: [('qBittorrent drive-a', True, [1]), ('qBittorrent drive-b', False, [2]), ('Transmission drive-a', True, [1]), ('Transmission drive-b', True, [2])]
|
||||
movie added: id=2 'Night of the Living Dead' (1968) path=/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968) tags=[2] profile=Any
|
||||
release push -> 200 approved=True rejections=[] downloadAllowed=True
|
||||
+13s grabbed: downloadClient=Transmission downloadClientName=Transmission drive-b downloadId=28766E9244F6920800B29298E9BBF00E41185CBA
|
||||
+63s queue: status=downloading trackedState=downloading left=595626753 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+123s queue: status=downloading trackedState=downloading left=328141569 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+183s queue: status=downloading trackedState=downloading left=50383617 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+243s queue: status=downloading trackedState=downloading left=50383617 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+303s queue: status=downloading trackedState=downloading left=35585 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+353s history downloadFolderImported: {"downloadClient": "Transmission", "downloadClientName": "Transmission drive-b", "droppedPath": "/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4", "importedPath": "/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4", "size": "596645320"}
|
||||
download file: /mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4 links=2 inode=131097 size=596645320
|
||||
library file : /mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 links=2 inode=131097 size=596645320
|
||||
RESULT s2-trans: HARDLINK (same inode); download on media_b, library on media_b
|
||||
[finished]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
== drive adatlemez (/mnt/felhom-drives/adatlemez/userdata/media)
|
||||
== drive media_a (/mnt/felhom-drives/media_a/userdata/media)
|
||||
== drive media_b (/mnt/felhom-drives/media_b/userdata/media)
|
||||
-- movies/ (1 file(s))
|
||||
596.6 MB links=2 inode=131097 movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4
|
||||
-- torrents/ (2 file(s))
|
||||
0.0 MB links=1 inode=131104 torrents/movies/.28766e9244f6920800b29298e9bbf00e41185cba.parts
|
||||
596.6 MB links=2 inode=131097 torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
-- transmission/ (4 file(s))
|
||||
596.6 MB links=1 inode=131100 transmission/s2-helper/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
0.0 MB links=1 inode=131101 transmission/s2-helper/night-of-the-living-dead-1968_202508/__ia_thumb.jpg
|
||||
0.0 MB links=1 inode=131102 transmission/s2-helper/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.sqlite
|
||||
0.0 MB links=1 inode=131103 transmission/s2-helper/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.xml
|
||||
|
||||
== 7 file(s) on 3 drive(s)
|
||||
== ONE FILE, TWO NAMES (hardlink — no extra copy):
|
||||
drive media_b inode 131097 (596.6 MB): movies/…/night-of-the-living-dead-1968_202508.mp4 <-> torrents/…/Night of the Living Dead 1968.mp4
|
||||
1 pair(s)
|
||||
== LIBRARY FILES WITH LINK COUNT 1 (a copy, or its download is gone):
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
== drive adatlemez (/mnt/felhom-drives/adatlemez/userdata/media)
|
||||
== drive media_a (/mnt/felhom-drives/media_a/userdata/media)
|
||||
== drive media_b (/mnt/felhom-drives/media_b/userdata/media)
|
||||
-- movies/ (1 file(s))
|
||||
596.6 MB links=2 inode=131097 movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4
|
||||
-- transmission/ (4 file(s))
|
||||
596.6 MB links=2 inode=131097 transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
0.0 MB links=1 inode=131098 transmission/movies/night-of-the-living-dead-1968_202508/__ia_thumb.jpg
|
||||
0.0 MB links=1 inode=131099 transmission/movies/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.sqlite
|
||||
0.0 MB links=1 inode=131100 transmission/movies/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.xml
|
||||
|
||||
== 5 file(s) on 3 drive(s)
|
||||
== ONE FILE, TWO NAMES (hardlink — no extra copy):
|
||||
drive media_b inode 131097 (596.6 MB): movies/…/night-of-the-living-dead-1968_202508.mp4 <-> transmission/…/Night of the Living Dead 1968.mp4
|
||||
1 pair(s)
|
||||
== LIBRARY FILES WITH LINK COUNT 1 (a copy, or its download is gone):
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
=== s3-mis: root=/mnt/felhom-drives/media_b/userdata/media/movies tag=drive-a client=(by tag) item=archive.org/night-of-the-living-dead-1968_202508
|
||||
clients: [('qBittorrent drive-a', True, [1]), ('qBittorrent drive-b', True, [2]), ('Transmission drive-a', True, [1]), ('Transmission drive-b', True, [2])]
|
||||
movie added: id=5 'Night of the Living Dead' (1968) path=/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968) tags=[1] profile=Any
|
||||
release push -> 200 approved=True rejections=[] downloadAllowed=True
|
||||
+14s grabbed: downloadClient=qBittorrent downloadClientName=qBittorrent drive-a downloadId=28766E9244F6920800B29298E9BBF00E41185CBA
|
||||
+64s queue: status=downloading trackedState=downloading left=595528449 client=qBittorrent drive-a output=None msgs=[]
|
||||
+124s queue: status=downloading trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+185s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+245s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+305s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+365s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+425s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+485s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+545s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+605s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+665s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+725s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+785s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+805s history downloadFolderImported: {"downloadClient": "qBittorrent", "downloadClientName": "qBittorrent drive-a", "droppedPath": "/mnt/felhom-drives/media_a/userdata/media/torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4", "importedPath": "/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4", "size": "596645320"}
|
||||
download file: /mnt/felhom-drives/media_a/userdata/media/torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4 links=1 inode=4194355 size=596645320
|
||||
library file : /mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 links=1 inode=131097 size=596645320
|
||||
RESULT s3-mis: COPY (two inodes); download on media_a, library on media_b
|
||||
[finished]
|
||||
|
||||
# NOTE: qBittorrent drive-a stalled on the LAST piece again (archive.org web seed, as in S2 attempt 2). At 17:21 CC added
|
||||
# the same public-domain torrent to Transmission (media_a/.../transmission/s3-helper) and gave qBittorrent its address
|
||||
# as a peer; qBittorrent completed at 17:23 and Radarr imported. Routing and import are unaffected by the helper.
|
||||
# Radarr's own log around the import:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
=== s3-tag: root=/mnt/felhom-drives/media_a/userdata/media/movies tag=drive-a client=(by tag) item=archive.org/night-of-the-living-dead-1968_202508
|
||||
clients: [('qBittorrent drive-a', True, [1]), ('qBittorrent drive-b', True, [2]), ('Transmission drive-a', True, [1]), ('Transmission drive-b', True, [2])]
|
||||
movie added: id=4 'Night of the Living Dead' (1968) path=/mnt/felhom-drives/media_a/userdata/media/movies/Night of the Living Dead (1968) tags=[1] profile=Any
|
||||
release push -> 200 approved=True rejections=[] downloadAllowed=True
|
||||
+11s grabbed: downloadClient=Transmission downloadClientName=Transmission drive-a downloadId=28766E9244F6920800B29298E9BBF00E41185CBA
|
||||
+61s queue: status=downloading trackedState=downloading left=587385601 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+122s queue: status=downloading trackedState=downloading left=309381889 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+182s queue: status=downloading trackedState=downloading left=52677377 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+242s queue: status=downloading trackedState=downloading left=52677377 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+302s queue: status=downloading trackedState=downloading left=35585 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+362s history downloadFolderImported: {"downloadClient": "Transmission", "downloadClientName": "Transmission drive-a", "droppedPath": "/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4", "importedPath": "/mnt/felhom-drives/media_a/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4", "size": "596645320"}
|
||||
download file: /mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4 links=2 inode=4194354 size=596645320
|
||||
library file : /mnt/felhom-drives/media_a/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 links=2 inode=4194354 size=596645320
|
||||
RESULT s3-tag: HARDLINK (same inode); download on media_a, library on media_a
|
||||
[finished]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
== drive adatlemez (/mnt/felhom-drives/adatlemez/userdata/media)
|
||||
== drive media_a (/mnt/felhom-drives/media_a/userdata/media)
|
||||
-- torrents/ (4 file(s))
|
||||
596.6 MB links=1 inode=4194355 torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
0.0 MB links=1 inode=4194364 torrents/movies/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.xml
|
||||
0.0 MB links=1 inode=4194362 torrents/movies/night-of-the-living-dead-1968_202508/__ia_thumb.jpg
|
||||
0.0 MB links=1 inode=4194363 torrents/movies/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.sqlite
|
||||
-- transmission/ (4 file(s))
|
||||
596.6 MB links=1 inode=4194358 transmission/s3-helper/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
0.0 MB links=1 inode=4194361 transmission/s3-helper/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.xml
|
||||
0.0 MB links=1 inode=4194359 transmission/s3-helper/night-of-the-living-dead-1968_202508/__ia_thumb.jpg
|
||||
0.0 MB links=1 inode=4194360 transmission/s3-helper/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.sqlite
|
||||
== drive media_b (/mnt/felhom-drives/media_b/userdata/media)
|
||||
-- movies/ (1 file(s))
|
||||
596.6 MB links=1 inode=131097 movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4
|
||||
|
||||
== 9 file(s) on 3 drive(s)
|
||||
== ONE FILE, TWO NAMES (hardlink — no extra copy):
|
||||
0 pair(s)
|
||||
== LIBRARY FILES WITH LINK COUNT 1 (a copy, or its download is gone):
|
||||
drive media_b: media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 (596.6 MB)
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""chain.py <run> — S2/S3: one public-domain film through the WHOLE path, run INSIDE the guest (run_in_guest.py).
|
||||
|
||||
Radarr (movie added with a root folder + tag) -> release push API -> download client (picked by the tag; for S2 the
|
||||
other same-tag client is switched off for the grab so the named one is used) -> download on a drive -> Radarr's
|
||||
completed-download import -> library file. Then the evidence: download path, library path, link counts, inodes,
|
||||
Radarr's history entry.
|
||||
|
||||
Content: „Night of the Living Dead" (1968), public domain (archive.org items carry the CC Public Domain Mark), < 1 GB.
|
||||
|
||||
runs:
|
||||
s2-qbit root B, tag drive-b, only „qBittorrent drive-b" enabled among the drive-b clients
|
||||
s2-trans root B, tag drive-b, only „Transmission drive-b" enabled among the drive-b clients
|
||||
s3-tag root A, tag drive-a, both drive-a clients enabled (the tag alone picks)
|
||||
s3-mis root B, tag drive-a (the mismatch)
|
||||
cleanup delete the movie with its files in Radarr, remove every torrent with its data in both clients
|
||||
Every key/password is read at runtime and redacted from the output.
|
||||
"""
|
||||
import base64, datetime, json, os, re, subprocess, sys, time, urllib.error, urllib.request
|
||||
|
||||
SECRETS = []
|
||||
TMDB = 10331
|
||||
ITEMS = {"s2-qbit": "night-of-the-living-dead-1968_202508", "s2-trans": "night-of-the-living-dead-1968_202508",
|
||||
"s3-tag": "night-of-the-living-dead-1968_202508", "s3-mis": "night-of-the-living-dead-1968_202508"}
|
||||
RUNS = {"s2-qbit": ("media_b", "drive-b", "qBittorrent drive-b"), "s2-trans": ("media_b", "drive-b", "Transmission drive-b"),
|
||||
"s3-tag": ("media_a", "drive-a", None), "s3-mis": ("media_b", "drive-a", None)}
|
||||
|
||||
|
||||
def red(s):
|
||||
s = str(s)
|
||||
for x in SECRETS:
|
||||
if x:
|
||||
s = s.replace(x, "<redacted>")
|
||||
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}}}}'").split()[0]
|
||||
|
||||
|
||||
def call(method, url, headers=None, data=None, form=False):
|
||||
h = dict(headers or {})
|
||||
if data is not None:
|
||||
if form:
|
||||
data = "&".join(f"{k}={urllib.request.quote(str(v))}" for k, v in data.items())
|
||||
h["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
else:
|
||||
data = json.dumps(data)
|
||||
h["Content-Type"] = "application/json"
|
||||
data = data.encode()
|
||||
req = urllib.request.Request(url, data=data, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
code, txt, hd = r.status, r.read().decode("utf-8", "replace"), {k.lower(): v for k, v in r.headers.items()}
|
||||
except urllib.error.HTTPError as e:
|
||||
code, txt, hd = e.code, e.read().decode("utf-8", "replace"), {k.lower(): v for k, v in e.headers.items()}
|
||||
try:
|
||||
body = json.loads(txt) if txt else None
|
||||
except ValueError:
|
||||
body = txt
|
||||
return code, body, hd
|
||||
|
||||
|
||||
key = re.search(r"<ApiKey>([^<]+)</ApiKey>", open("/var/lib/docker/volumes/radarr_radarr_config/_data/config.xml").read()).group(1)
|
||||
SECRETS.append(key)
|
||||
RA = f"http://{ip('radarr')}:7878/api/v3"
|
||||
H = {"X-Api-Key": key}
|
||||
|
||||
|
||||
def ra(method, path, data=None):
|
||||
c, b, _ = call(method, RA + path, H, data)
|
||||
if c >= 300:
|
||||
say(f" !! radarr {method} {path} -> {c} {red(json.dumps(b))[:500]}")
|
||||
return c, b
|
||||
|
||||
|
||||
def movie():
|
||||
c, ms = ra("GET", "/movie")
|
||||
return next((m for m in ms if m.get("tmdbId") == TMDB), None)
|
||||
|
||||
|
||||
def clients():
|
||||
return ra("GET", "/downloadclient")[1]
|
||||
|
||||
|
||||
def set_enable(cl, on):
|
||||
if cl["enable"] != on:
|
||||
cl = dict(cl, enable=on)
|
||||
ra("PUT", f"/downloadclient/{cl['id']}", cl)
|
||||
|
||||
|
||||
def tag_id(label):
|
||||
return next(t["id"] for t in ra("GET", "/tag")[1] if t["label"] == label)
|
||||
|
||||
|
||||
def st(p):
|
||||
try:
|
||||
s = os.stat(p)
|
||||
return f"links={s.st_nlink} inode={s.st_ino} size={s.st_size}"
|
||||
except OSError as e:
|
||||
return f"stat failed: {e}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- client side (for cleanup and the download path)
|
||||
def qb():
|
||||
QB = f"http://{ip('qbittorrent')}:8080"
|
||||
pw = sh("docker exec qbittorrent printenv QBT_ADMIN_PASSWORD"); SECRETS.append(pw)
|
||||
c, _, hd = call("POST", QB + "/api/v2/auth/login", {"Referer": QB}, {"username": "admin", "password": pw}, form=True)
|
||||
m = re.search(r"((?:QBT_)?SID[A-Za-z0-9_]*)=([^;]+)", hd.get("set-cookie", "")); SECRETS.append(m.group(2))
|
||||
return QB, {"Referer": QB, "Cookie": f"{m.group(1)}={m.group(2)}"}
|
||||
|
||||
|
||||
def tr(method, args=None):
|
||||
TR = f"http://{ip('transmission')}:9091/transmission/rpc"
|
||||
pw = sh("docker exec transmission printenv PASS"); SECRETS.append(pw)
|
||||
h = {"Authorization": "Basic " + base64.b64encode(f"admin:{pw}".encode()).decode()}
|
||||
SECRETS.append(h["Authorization"].split()[1])
|
||||
c, b, hd = call("POST", TR, h, {"method": method, "arguments": args or {}})
|
||||
if c == 409:
|
||||
h["X-Transmission-Session-Id"] = hd.get("x-transmission-session-id", "")
|
||||
c, b, hd = call("POST", TR, h, {"method": method, "arguments": args or {}})
|
||||
return b
|
||||
|
||||
|
||||
def cleanup():
|
||||
m = movie()
|
||||
if m:
|
||||
ra("DELETE", f"/movie/{m['id']}?deleteFiles=true&addImportExclusion=false")
|
||||
say(f"radarr: movie {m['id']} deleted with its files")
|
||||
QB, QH = qb()
|
||||
c, ts, _ = call("GET", QB + "/api/v2/torrents/info", QH)
|
||||
for t in ts or []:
|
||||
call("POST", QB + "/api/v2/torrents/delete", QH, {"hashes": t["hash"], "deleteFiles": "true"}, form=True)
|
||||
say(f"qbittorrent: removed {t['name']} with its data ({t['content_path']})")
|
||||
b = tr("torrent-get", {"fields": ["id", "name", "downloadDir"]})
|
||||
for t in (b or {}).get("arguments", {}).get("torrents", []):
|
||||
tr("torrent-remove", {"ids": [t["id"]], "delete-local-data": True})
|
||||
say(f"transmission: removed {t['name']} with its data ({t['downloadDir']})")
|
||||
for c in ra("GET", "/downloadclient")[1]:
|
||||
set_enable(c, True)
|
||||
say("all download clients enabled again")
|
||||
|
||||
|
||||
def run(name):
|
||||
drive, tag, only = RUNS[name]
|
||||
item = ITEMS[name]
|
||||
root = f"/mnt/felhom-drives/{drive}/userdata/media/movies"
|
||||
say(f"=== {name}: root={root} tag={tag} client={'(by tag)' if not only else only} item=archive.org/{item}")
|
||||
m = movie()
|
||||
if m:
|
||||
sys.exit(say("STOP: the film is already in Radarr — run cleanup first") or 1)
|
||||
cls = clients()
|
||||
if only:
|
||||
for c in cls:
|
||||
if tag_id(tag) in c["tags"]:
|
||||
set_enable(c, c["name"] == only)
|
||||
say(" clients:", [(c["name"], c["enable"], c["tags"]) for c in clients()])
|
||||
c, lk = ra("GET", f"/movie/lookup/tmdb?tmdbId={TMDB}")
|
||||
qp = ra("GET", "/qualityprofile")[1][0]
|
||||
body = dict(lk, qualityProfileId=qp["id"], rootFolderPath=root, monitored=True, tags=[tag_id(tag)],
|
||||
minimumAvailability="released", addOptions={"searchForMovie": False})
|
||||
c, m = ra("POST", "/movie", body)
|
||||
say(f" movie added: id={m['id']} '{m['title']}' ({m['year']}) path={m['path']} tags={m['tags']} profile={qp['name']}")
|
||||
sz = {"night-of-the-living-dead-1968_202312": 550604180, "night-of-the-living-dead-1968_202508": 596600000,
|
||||
"night-of-the-living-dead_202309": 598500000}[item]
|
||||
rel = {"title": "Night.of.the.Living.Dead.1968.480p.WEB-DL.x264-ARCHIVEORG",
|
||||
"downloadUrl": f"https://archive.org/download/{item}/{item}_archive.torrent",
|
||||
"protocol": "torrent", "publishDate": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"size": sz, "indexer": "archive.org (release push)"}
|
||||
t0 = time.time()
|
||||
c, d = ra("POST", "/release/push", rel)
|
||||
dd = d[0] if isinstance(d, list) else d
|
||||
say(f" release push -> {c} approved={dd.get('approved')} rejections={dd.get('rejections')} downloadAllowed={dd.get('downloadAllowed')}")
|
||||
grabbed = imported = None
|
||||
for i in range(240):
|
||||
time.sleep(10)
|
||||
c, h = ra("GET", f"/history/movie?movieId={m['id']}")
|
||||
for e in h or []:
|
||||
if e["eventType"] == "grabbed" and not grabbed:
|
||||
grabbed = e
|
||||
say(f" +{int(time.time()-t0)}s grabbed: downloadClient={e['data'].get('downloadClient')} "
|
||||
f"downloadClientName={e['data'].get('downloadClientName')} downloadId={e.get('downloadId')}")
|
||||
if e["eventType"] == "downloadFolderImported":
|
||||
imported = e
|
||||
if imported:
|
||||
break
|
||||
if i % 6 == 5:
|
||||
c, q = ra("GET", f"/queue?movieIds={m['id']}")
|
||||
for r in (q or {}).get("records", []):
|
||||
say(f" +{int(time.time()-t0)}s queue: status={r.get('status')} trackedState={r.get('trackedDownloadState')} "
|
||||
f"left={r.get('sizeleft')} client={r.get('downloadClient')} output={r.get('outputPath')} "
|
||||
f"msgs={[s.get('messages') for s in r.get('statusMessages', [])]}")
|
||||
if not imported:
|
||||
say(f" RESULT {name}: NOT imported within {int(time.time()-t0)}s")
|
||||
return
|
||||
e = imported["data"]
|
||||
say(f" +{int(time.time()-t0)}s history downloadFolderImported: {json.dumps({k: e.get(k) for k in ('downloadClient', 'downloadClientName', 'droppedPath', 'importedPath', 'size')})}")
|
||||
say(f" download file: {e['droppedPath']} {st(e['droppedPath'])}")
|
||||
say(f" library file : {e['importedPath']} {st(e['importedPath'])}")
|
||||
a, b = os.stat(e["droppedPath"]), os.stat(e["importedPath"])
|
||||
say(f" RESULT {name}: {'HARDLINK (same inode)' if (a.st_ino, a.st_dev) == (b.st_ino, b.st_dev) else 'COPY (two inodes)'}"
|
||||
f"; download on {e['droppedPath'].split('/')[3]}, library on {e['importedPath'].split('/')[3]}")
|
||||
for c in clients():
|
||||
set_enable(c, True)
|
||||
|
||||
|
||||
if sys.argv[1] == "cleanup":
|
||||
cleanup()
|
||||
else:
|
||||
run(sys.argv[1])
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""deploy.py <app> [<app> ...] — Part B4: install an app on the Tester 1 box the way the deploy wizard does:
|
||||
GET /api/stacks/<app>/deploy-fields, POST /api/stacks/<app>/deploy, then wait for the deploy's own completion mark
|
||||
(the pin in app.yaml), as box_walk.deploy does.
|
||||
|
||||
Values: DOMAIN, the app's default SUBDOMAIN, HDD_PATH = drive A (the drive the household would pick; the drill
|
||||
templates bind both drives by plain path and do not read it), and a generated value for each password field — kept
|
||||
ONLY in the 0600 file ~/.felhom-tester1/secrets.json (never printed, never in evidence). box_walk.deploy is not used
|
||||
because its deploy_values makes a folder on the scratch box's drive path, which does not exist here.
|
||||
|
||||
Live-probes rule: never send a deploy for an app that is not meant to be installed. This script refuses an app that is
|
||||
already installed and installs only the apps named on the command line.
|
||||
"""
|
||||
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")
|
||||
|
||||
|
||||
def load():
|
||||
try:
|
||||
return json.load(open(SEC))
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
|
||||
def save(d):
|
||||
fd = os.open(SEC, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(d, f)
|
||||
|
||||
|
||||
w.login()
|
||||
for app in sys.argv[1:]:
|
||||
st = w.stack(app)
|
||||
if st.get("deployed"):
|
||||
w.say(f"{app}: already installed — not touched")
|
||||
continue
|
||||
c, d = w.ctl("GET", f"/api/stacks/{app}/deploy-fields")
|
||||
fields = (((d.get("data") or {}).get("metadata") or {}).get("deploy_fields")) or []
|
||||
vals, gen = {"DOMAIN": w.DOMAIN}, []
|
||||
for f in fields:
|
||||
ev, ty = f.get("env_var"), f.get("type")
|
||||
if ev == "DOMAIN":
|
||||
continue
|
||||
if ev == "SUBDOMAIN":
|
||||
vals[ev] = f.get("default") or st.get("meta", {}).get("subdomain") or app
|
||||
elif ev == "HDD_PATH":
|
||||
vals[ev] = HDD
|
||||
elif ty == "password":
|
||||
vals[ev] = "T1-" + secrets.token_urlsafe(15)
|
||||
gen.append(ev)
|
||||
elif f.get("required"):
|
||||
sys.exit(w.say(f"{app}: required field {ev} ({ty}) has no rule here — STOP, not guessing") or 1)
|
||||
s = load(); s[app] = {k: vals[k] for k in gen}; save(s)
|
||||
shown = {k: ("<generated, kept in secrets.json>" if k in gen else v) for k, v in vals.items()}
|
||||
c, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": vals})
|
||||
w.say(f"{app}: POST deploy {json.dumps(shown)} -> {c} {json.dumps(d, ensure_ascii=False)[:200]}")
|
||||
if c != "202":
|
||||
continue
|
||||
for _ in range(120):
|
||||
time.sleep(5)
|
||||
st = w.stack(app)
|
||||
pins = (st.get("app_config") or {}).get("pinned_images")
|
||||
if st.get("deployed") and pins and st.get("state") in ("running", "unhealthy", "degraded"):
|
||||
w.say(f"{app}: deployed, state={st.get('state')}, pinned={pins}")
|
||||
break
|
||||
else:
|
||||
w.say(f"{app}: never became deployed (state={st.get('state')})")
|
||||
print(w.guest(f"docker inspect {app} -f '{{{{.State.Status}}}} {{{{range .Mounts}}}}{{{{.Source}}}}->{{{{.Destination}}}}:{{{{.RW}}}} {{{{end}}}}'; "
|
||||
f"docker logs --since 10m felhom-controller 2>&1 | grep -i '{app}' | grep -v DEBUG | grep -iE 'after_install|deploy|error|warn' | tail -8"))
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""enrol.py <device> <mount_name> <label> — Part A4: enrol one blank disk on the Tester 1 box through the SAME calls
|
||||
the „Új meghajtó inicializálása" wizard makes (storage_init.html): GET /api/disks/candidates, POST /api/storage/init,
|
||||
then poll GET /api/storage/init/status to a terminal phase. set_default is sent FALSE (the wizard's checkbox is
|
||||
checked by default — recorded in the report; this run keeps Tester 1's default drive unchanged).
|
||||
|
||||
Run from DooPlex: SC=~/.felhom-tester1 TARGET=tester-1 python3 enrol.py /dev/sdc media_a "Media A"
|
||||
"""
|
||||
import json, sys, time
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
|
||||
dev, name, label = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
w.login()
|
||||
c, d = w.ctl("GET", "/api/disks/candidates")
|
||||
cands = [x["device"] for x in ((d.get("data") or {}).get("initialize") or [])]
|
||||
w.say(f"candidates -> {c} {cands}")
|
||||
if dev not in cands:
|
||||
sys.exit(w.say(f"STOP: {dev} is not offered for initialisation") or 1)
|
||||
body = {"device": dev, "fstype": "ext4", "mount_name": name, "label": label,
|
||||
"set_default": False, "confirmed": False, "durable_id": ""}
|
||||
c, d = w.ctl("POST", "/api/storage/init", body)
|
||||
w.say(f"POST /api/storage/init {json.dumps(body)} -> {c} {json.dumps(d, ensure_ascii=False)}")
|
||||
if c != "200":
|
||||
sys.exit(1)
|
||||
last = None
|
||||
for _ in range(600):
|
||||
time.sleep(2)
|
||||
c, d = w.ctl("GET", "/api/storage/init/status")
|
||||
ph = (d.get("data") or {}).get("phase")
|
||||
if ph != last:
|
||||
w.say(f"status -> {c} {json.dumps(d.get('data'), ensure_ascii=False)}")
|
||||
last = ph
|
||||
if ph in ("done", "failed", "refused", "needs_confirmation"):
|
||||
break
|
||||
print("RESULT", last)
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_drill.py — Part B2: rewrite the six media templates IN THE DRILL CATALOG to the layout under test.
|
||||
|
||||
The layout (one bind per drive, the same path inside the container as in the guest, `userdata/media` only):
|
||||
|
||||
/mnt/felhom-drives/<drive>/userdata/media -> /mnt/felhom-drives/<drive>/userdata/media (rw; :ro for Plex/Jellyfin)
|
||||
|
||||
for <drive> in DRIVES. Images are NOT touched. Plain host paths on purpose: the controller's bind classifier knows only
|
||||
the ${..._PATH} roots (stacks/classify_binds.go classifyRoot), so these binds are in no app capture set (A6).
|
||||
|
||||
Because the ${USERDATA_PATH} binds go away, the `.felhom.yml` `backup.userdata` entries and `data_paths` that named them
|
||||
are removed too — appbackup.ValidateBackupSpec rejects an entry that matches no compose bind, and a rejected block
|
||||
degrades the app to the legacy path. qBittorrent's and Transmission's `backup.volumes` (their config) stay.
|
||||
|
||||
Default save paths: drive A's `media/torrents` (qBittorrent, seeded on first boot) and `media/transmission/complete`
|
||||
(Transmission, set by its entrypoint on a fresh config only). Everything else is unchanged.
|
||||
|
||||
python3 make_drill.py [--check] # edits /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates in place
|
||||
"""
|
||||
import re, sys
|
||||
|
||||
D = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates"
|
||||
DRIVES = ("media_a", "media_b")
|
||||
M = {d: f"/mnt/felhom-drives/{d}/userdata/media" for d in DRIVES}
|
||||
A = M["media_a"]
|
||||
|
||||
|
||||
def binds(indent, ro):
|
||||
suf = ":ro" if ro else ""
|
||||
return "".join(f"{indent}- {M[d]}:{M[d]}{suf}\n" for d in DRIVES)
|
||||
|
||||
|
||||
def sub1(s, old, new, app):
|
||||
if old not in s:
|
||||
sys.exit(f"{app}: expected text not found: {old!r}")
|
||||
return s.replace(old, new, 1)
|
||||
|
||||
|
||||
def compose(app):
|
||||
p = f"{D}/{app}/docker-compose.yml"
|
||||
s = open(p).read()
|
||||
if M["media_a"] in s:
|
||||
return f"{app}: compose already in the new layout"
|
||||
i = " "
|
||||
if app == "sonarr":
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/tv:/media/tv\n{i}- ${{USERDATA_PATH}}/downloads:/downloads\n", binds(i, False), app)
|
||||
elif app == "radarr":
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/movies:/media/movies\n{i}- ${{USERDATA_PATH}}/downloads:/downloads\n", binds(i, False), app)
|
||||
elif app == "qbittorrent":
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/torrents:/downloads\n", binds(i, False), app)
|
||||
s = sub1(s, "'Session\\DefaultSavePath=/downloads'", f"'Session\\DefaultSavePath={A}/torrents'", app)
|
||||
s = sub1(s, "'Session\\TempPath=/downloads/.temp'", f"'Session\\TempPath={A}/torrents/.temp'", app)
|
||||
elif app == "transmission":
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/transmission:/downloads\n", binds(i, False), app)
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/transmission/watch:/watch\n", f"{i}- {A}/transmission/watch:/watch\n", app)
|
||||
# the default download folders are set ONCE, on a fresh config (a household's later choice is kept)
|
||||
s = sub1(s, """ cp /defaults/settings.json "$$CONF"\n""",
|
||||
f""" cp /defaults/settings.json "$$CONF"\n jq '.["download-dir"] = "{A}/transmission/complete" | .["incomplete-dir"] = "{A}/transmission/incomplete"' "$$CONF" > /tmp/d.json && mv /tmp/d.json "$$CONF"\n""", app)
|
||||
elif app in ("plex", "jellyfin"):
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media:/media:ro\n", binds(i, True), app)
|
||||
if "${USERDATA_PATH}" in re.sub(r"^#.*$", "", s, flags=re.M):
|
||||
sys.exit(f"{app}: a ${{USERDATA_PATH}} bind is left")
|
||||
open(p, "w").write(s)
|
||||
return f"{app}: compose rewritten"
|
||||
|
||||
|
||||
def felhom_yml(app):
|
||||
p = f"{D}/{app}/.felhom.yml"
|
||||
s = open(p).read()
|
||||
before = s
|
||||
# drop backup.userdata (the list under it) — and the whole backup block when nothing else is left in it
|
||||
s = re.sub(r"(^backup:\n(?: volumes:\n(?: .*\n| .*\n| .*\n)*)?) userdata:\n(?: .*\n| .*\n)+", r"\1", s, flags=re.M)
|
||||
s = re.sub(r"^# --- Backup classification[^\n]*\n^backup:\n(?=\n|#|[a-z])", "", s, flags=re.M)
|
||||
# drop data_paths (top level only; the i18n.en copy is display text and names no bind)
|
||||
s = re.sub(r"^# --- Customer-facing folder annotation[^\n]*\n^data_paths:\n(?: .*\n)+", "", s, flags=re.M)
|
||||
if s != before:
|
||||
open(p, "w").write(s)
|
||||
return f"{app}: .felhom.yml backup.userdata/data_paths removed"
|
||||
return f"{app}: .felhom.yml unchanged"
|
||||
|
||||
|
||||
for app in ("sonarr", "radarr", "qbittorrent", "transmission", "plex", "jellyfin"):
|
||||
print(compose(app))
|
||||
print(felhom_yml(app))
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""repoint.py mirror|drill|restore|status — point the Tester 1 box at the drill catalog WITHOUT a credential (R-992).
|
||||
|
||||
WHY A LOCAL MIRROR. `09` §6.5's route needs a catalog credential on the box: the drill repo is private (R-992). This
|
||||
session could not mint a READ-ONLY one — the Gitea credential on DooPlex is itself a token, and Gitea answers 403 to a
|
||||
token asking to list or create tokens (measured 2026-10-14) — and the night of 2026-10-06 put the admin WRITE token
|
||||
into the box's controller.yaml, which this task forbids. The cache-staging workaround of 2026-10-12 does not hold for a
|
||||
test that lasts days: every sync (and every controller start) re-applies the live catalog over the staged templates
|
||||
and over the installed apps' compose files (Syncer.copyTemplates, no deployed check).
|
||||
|
||||
So the drill catalog is MIRRORED onto the box: a bare git repository inside the controller's own data volume, and
|
||||
`git.repo_url: file:///opt/docker/felhom-controller/drill-catalog.git`. The controller's sync runs exactly as for a
|
||||
remote (clone/fetch/reset, the R-615 re-clone on a changed URL); no secret is on the box. DooPlex updates the mirror
|
||||
from the drill repo with a git bundle (`mirror`). The drill repo on Gitea stays the record.
|
||||
|
||||
SC=~/.felhom-tester1 TARGET=tester-1 python3 repoint.py mirror # (re)load the mirror from the drill repo main
|
||||
... repoint.py drill # save controller.yaml (once), set repo_url to the mirror, restart, sync, rescan
|
||||
... repoint.py restore # put the saved controller.yaml back (byte-identical), restart, sync, rescan
|
||||
... repoint.py status
|
||||
"""
|
||||
import os, subprocess, sys, time
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
|
||||
DRILL = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill"
|
||||
VOL = "/var/lib/docker/volumes/felhom-controller-data/_data" # guest path of the controller's data volume
|
||||
CVOL = "/opt/docker/felhom-controller" # the same, inside the controller container
|
||||
SAVE = f"{VOL}/controller.yaml.pre-media-2026-10-14"
|
||||
MIRROR_URL = f"file://{CVOL}/drill-catalog.git"
|
||||
|
||||
|
||||
def status():
|
||||
print(w.guest(f"""grep -nE 'repo_url|sync_interval' {VOL}/controller.yaml; ls -l {SAVE} 2>&1 | tail -1
|
||||
docker exec felhom-controller sh -c 'git -C {CVOL}/drill-catalog.git log -1 --format="mirror main %h %s" main 2>&1;
|
||||
git -C {CVOL}/data/catalog-cache log -1 --format="cache HEAD %h %s" 2>&1; git -C {CVOL}/data/catalog-cache remote get-url origin'"""))
|
||||
|
||||
|
||||
def mirror():
|
||||
b = "/tmp/drill-media.bundle"
|
||||
subprocess.run(["git", "-C", DRILL, "bundle", "create", b, "main"], check=True, capture_output=True)
|
||||
head = subprocess.run(["git", "-C", DRILL, "rev-parse", "--short", "main"], capture_output=True, text=True).stdout.strip()
|
||||
ssh = w.ssh_args()
|
||||
# bundle -> PVE host -> guest -> controller volume; the bare repo is made and fetched BY the controller's own user
|
||||
subprocess.run(["scp", "-q", "-o", "StrictHostKeyChecking=accept-new"] + (["-J", w._T["jump"]] if w._T["jump"] else [])
|
||||
+ [b, f"{w._T['ssh']}:{b}"], check=True)
|
||||
subprocess.run(ssh + [f"pct push {w.GUEST} {b} {VOL}/drill-media.bundle && rm -f {b}"], check=True)
|
||||
print(w.guest(f"""set -e
|
||||
docker exec felhom-controller sh -c 'test -d {CVOL}/drill-catalog.git || git init -q --bare -b main {CVOL}/drill-catalog.git
|
||||
git -C {CVOL}/drill-catalog.git fetch -q {CVOL}/drill-media.bundle +main:main && rm -f {CVOL}/drill-media.bundle
|
||||
git -C {CVOL}/drill-catalog.git log -1 --format="mirror main %h %s" main'"""))
|
||||
print("drill repo main:", head)
|
||||
os.remove(b)
|
||||
|
||||
|
||||
def restart_sync():
|
||||
w.guest("docker restart felhom-controller >/dev/null")
|
||||
for _ in range(60):
|
||||
time.sleep(3)
|
||||
try:
|
||||
w.login()
|
||||
break
|
||||
except SystemExit:
|
||||
continue
|
||||
w.sync_rescan()
|
||||
time.sleep(3)
|
||||
print(w.guest("docker logs --since 3m felhom-controller 2>&1 | grep -E '\\[sync\\]' | grep -v DEBUG | tail -8"))
|
||||
|
||||
|
||||
def drill():
|
||||
print(w.guest(f"""set -e
|
||||
test -f {SAVE} || cp -p {VOL}/controller.yaml {SAVE}
|
||||
sed -i -E 's#^(\\s+repo_url: ).*$#\\1{MIRROR_URL}#' {VOL}/controller.yaml
|
||||
grep -n 'repo_url' {VOL}/controller.yaml; diff {SAVE} {VOL}/controller.yaml || true"""))
|
||||
restart_sync()
|
||||
status()
|
||||
|
||||
|
||||
def restore():
|
||||
print(w.guest(f"""set -e
|
||||
cp -p {SAVE} {VOL}/controller.yaml
|
||||
cmp {SAVE} {VOL}/controller.yaml && echo RESTORED-IDENTICAL; grep -n repo_url {VOL}/controller.yaml"""))
|
||||
restart_sync()
|
||||
status()
|
||||
|
||||
|
||||
{"mirror": mirror, "drill": drill, "restore": restore, "status": status}[sys.argv[1]]()
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""run_bg.py start <script.py> <arg> | log <arg> | stop <arg> — run a long chain step DETACHED inside the Tester 1 guest
|
||||
(so a dropped SSH does not kill it), and read its log. The log lives in /root/media-<arg>.log until `stop` removes it
|
||||
together with the script; the caller copies the log into the evidence folder first."""
|
||||
import os, sys
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
op = sys.argv[1]
|
||||
if op == "start":
|
||||
src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), sys.argv[2])).read()
|
||||
a = sys.argv[3]
|
||||
print(w.guest(f"cat > /root/.media-{a}.py <<'PYEOF'\n{src}\nPYEOF\n"
|
||||
f"nohup python3 -u /root/.media-{a}.py {a} > /root/media-{a}.log 2>&1 < /dev/null &\necho started"))
|
||||
elif op == "log":
|
||||
print(w.guest(f"cat /root/media-{sys.argv[2]}.log; pgrep -f '/root/.media-{sys.argv[2]}.py' >/dev/null && echo '[still running]' || echo '[finished]'"))
|
||||
elif op == "stop":
|
||||
print(w.guest(f"pkill -f 'python3 -u /root/.media-{sys.argv[2]}.py'; rm -f /root/.media-{sys.argv[2]}.py /root/media-{sys.argv[2]}.log; echo removed"))
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""run_in_guest.py <script.py> [args] — run one of this folder's stdlib scripts INSIDE the Tester 1 guest
|
||||
(SC=~/.felhom-tester1 TARGET=tester-1) and remove it afterwards. Output only; nothing is left on the box."""
|
||||
import os, sys
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), sys.argv[1])).read()
|
||||
args = " ".join(sys.argv[2:])
|
||||
print(w.guest(f"cat > /root/.media-tool.py <<'PYEOF'\n{src}\nPYEOF\npython3 /root/.media-tool.py {args} 2>&1; rm -f /root/.media-tool.py", timeout=1800))
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""where.py — S4: where every media file sits, per drive. Run INSIDE the guest (run_in_guest.py where.py).
|
||||
|
||||
For each enrolled drive under /mnt/felhom-drives/*/userdata/media: every regular file (hidden temp folders included)
|
||||
with its size, link count and inode, grouped by top folder (tv, movies, torrents, transmission, ...). Then the pairs
|
||||
that are ONE file (same device + inode = a hardlink: no extra copy) and the library files that are NOT linked to any
|
||||
download (link count 1 = a copy, or a download already removed). Stdlib only; reads, never writes.
|
||||
"""
|
||||
import collections, glob, os
|
||||
|
||||
ROOTS = sorted(glob.glob("/mnt/felhom-drives/*/userdata/media"))
|
||||
by_ino = collections.defaultdict(list)
|
||||
total = 0
|
||||
for root in ROOTS:
|
||||
drive = root.split("/")[3]
|
||||
print(f"== drive {drive} ({root})")
|
||||
for top in sorted(os.listdir(root)):
|
||||
rows = []
|
||||
for dp, dn, fn in os.walk(os.path.join(root, top)):
|
||||
for f in fn:
|
||||
p = os.path.join(dp, f)
|
||||
try:
|
||||
s = os.lstat(p)
|
||||
except OSError as e:
|
||||
rows.append(f" ?? {p}: {e}")
|
||||
continue
|
||||
if not os.path.isfile(p) or os.path.islink(p):
|
||||
continue
|
||||
total += 1
|
||||
by_ino[(s.st_dev, s.st_ino)].append((drive, top, p, s.st_size, s.st_nlink))
|
||||
rows.append(f" {s.st_size/1e6:10.1f} MB links={s.st_nlink} inode={s.st_ino:<10} {os.path.relpath(p, root)}")
|
||||
if rows:
|
||||
print(f" -- {top}/ ({len(rows)} file(s))")
|
||||
print("\n".join(rows))
|
||||
print(f"\n== {total} file(s) on {len(ROOTS)} drive(s)")
|
||||
print("== ONE FILE, TWO NAMES (hardlink — no extra copy):")
|
||||
n = 0
|
||||
for k, v in by_ino.items():
|
||||
if len(v) > 1:
|
||||
n += 1
|
||||
print(f" drive {v[0][0]} inode {k[1]} ({v[0][3]/1e6:.1f} MB): " + " <-> ".join(f"{x[1]}/…/{os.path.basename(x[2])}" for x in v))
|
||||
print(f" {n} pair(s)")
|
||||
print("== LIBRARY FILES WITH LINK COUNT 1 (a copy, or its download is gone):")
|
||||
for k, v in by_ino.items():
|
||||
for d, top, p, size, nl in v:
|
||||
if top in ("tv", "movies") and nl == 1:
|
||||
print(f" drive {d}: {os.path.relpath(p, '/mnt/felhom-drives')} ({size/1e6:.1f} MB)")
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wire.py [-v] — Part C: connect Sonarr and Radarr to qBittorrent and Transmission, ONE ENTRY PER DRIVE, on the box.
|
||||
|
||||
Runs INSIDE the guest (it talks to the containers on the docker network and reads their keys from their own config
|
||||
volumes / environment). Stdlib only. Every key and password is read at runtime and NEVER written to a file or a log:
|
||||
every printed line passes through redact(). `-v` prints every call and the SHAPE of every answer (status, top-level
|
||||
keys / list length) — this is the reference the later link code copies.
|
||||
|
||||
For each drive d in DRIVES, M = /mnt/felhom-drives/<d>/userdata/media (the same path in every container):
|
||||
qBittorrent : categories tv-<d>, movies-<d> -> save path M/torrents/{tv,movies}, incomplete path M/torrents/.temp
|
||||
Sonarr : root folder M/tv; tag <d>; „qBittorrent <d>" (category tv-<d>, tag <d>) and
|
||||
„Transmission <d>" (directory M/transmission/tv, tag <d>)
|
||||
Radarr : the same with movies
|
||||
Sonarr/Radarr: minimum free space when importing = MIN_FREE_MB; „use hardlinks instead of copy" quoted
|
||||
Box-wide, recorded with the reason in the output:
|
||||
qBittorrent : auto_tmm_enabled = true (a category's save path applies ONLY in automatic torrent management;
|
||||
qBittorrent's default is manual, which saves every torrent to the default
|
||||
path = drive A, whatever the category says)
|
||||
Transmission: incomplete-dir-enabled = false (Transmission has ONE incomplete folder for all torrents; on drive A it
|
||||
would make every drive-B download a cross-drive copy at completion)
|
||||
|
||||
Idempotent: an existing tag / root folder / client / category of the same name is left as it is and reported.
|
||||
"""
|
||||
import base64, json, re, subprocess, sys, urllib.error, urllib.parse, urllib.request
|
||||
|
||||
V = "-v" in sys.argv
|
||||
DRIVES = [("drive-a", "media_a"), ("drive-b", "media_b")]
|
||||
MIN_FREE_MB = 10240
|
||||
SECRETS = []
|
||||
|
||||
|
||||
def redact(s):
|
||||
s = str(s)
|
||||
for x in SECRETS:
|
||||
if x:
|
||||
s = s.replace(x, "<redacted>")
|
||||
return s
|
||||
|
||||
|
||||
def say(*a):
|
||||
print(redact(" ".join(str(x) for x in a)), flush=True)
|
||||
|
||||
|
||||
def sh(cmd):
|
||||
return subprocess.run(cmd, shell=True, capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def shape(body):
|
||||
if isinstance(body, dict):
|
||||
return "{" + ", ".join(sorted(body.keys()))[:300] + "}"
|
||||
if isinstance(body, list):
|
||||
return f"[list of {len(body)}]"
|
||||
return repr(body)[:120]
|
||||
|
||||
|
||||
def call(method, url, headers=None, data=None, form=False, raw=False, expect=(200, 201, 202, 204)):
|
||||
h = dict(headers or {})
|
||||
if data is not None and not isinstance(data, (bytes, str)):
|
||||
data = urllib.parse.urlencode(data) if form else json.dumps(data)
|
||||
h.setdefault("Content-Type", "application/x-www-form-urlencoded" if form else "application/json")
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
req = urllib.request.Request(url, data=data, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
code, txt, hdrs = r.status, r.read().decode("utf-8", "replace"), {k.lower(): v for k, v in r.headers.items()}
|
||||
except urllib.error.HTTPError as e:
|
||||
code, txt, hdrs = e.code, e.read().decode("utf-8", "replace"), {k.lower(): v for k, v in e.headers.items()}
|
||||
try:
|
||||
body = json.loads(txt) if txt and not raw else txt
|
||||
except ValueError:
|
||||
body = txt
|
||||
if V:
|
||||
say(f" {method} {url} -> {code} {shape(body)}")
|
||||
if code not in expect:
|
||||
say(f" !! {method} {url} -> {code}: {redact(txt)[:600]}")
|
||||
return code, body, hdrs
|
||||
|
||||
|
||||
def ip(c):
|
||||
return sh(f"docker inspect {c} -f '{{{{range .NetworkSettings.Networks}}}}{{{{.IPAddress}}}} {{{{end}}}}'").split()[0]
|
||||
|
||||
|
||||
def apikey(app):
|
||||
x = open(f"/var/lib/docker/volumes/{app}_{app}_config/_data/config.xml").read()
|
||||
k = re.search(r"<ApiKey>([^<]+)</ApiKey>", x).group(1)
|
||||
SECRETS.append(k)
|
||||
return k
|
||||
|
||||
|
||||
def env(c, var):
|
||||
v = sh(f"docker exec {c} printenv {var}")
|
||||
SECRETS.append(v)
|
||||
return v
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- qBittorrent
|
||||
QB = f"http://{ip('qbittorrent')}:8080"
|
||||
qpw = env("qbittorrent", "QBT_ADMIN_PASSWORD")
|
||||
say("== qBittorrent", QB, "(password: the deploy secret, read from the container's environment)")
|
||||
c, _, hd = call("POST", QB + "/api/v2/auth/login", {"Referer": QB}, {"username": "admin", "password": qpw}, form=True, raw=True)
|
||||
# qBittorrent 5.x names the session cookie QBT_SID_<port> (older versions: SID) — take whichever it set
|
||||
m = re.search(r"((?:QBT_)?SID[A-Za-z0-9_]*)=([^;]+)", hd.get("set-cookie", ""))
|
||||
if not m:
|
||||
sys.exit(say(f" !! no session cookie; answer headers: {sorted(hd)}") or 1)
|
||||
say(f" session cookie name: {m.group(1)}")
|
||||
SECRETS.append(m.group(2))
|
||||
QH = {"Referer": QB, "Cookie": f"{m.group(1)}={m.group(2)}"}
|
||||
c, prefs, _ = call("GET", QB + "/api/v2/app/preferences", QH)
|
||||
say(f" before: auto_tmm_enabled={prefs.get('auto_tmm_enabled')} save_path={prefs.get('save_path')} "
|
||||
f"temp_path_enabled={prefs.get('temp_path_enabled')} temp_path={prefs.get('temp_path')}")
|
||||
call("POST", QB + "/api/v2/app/setPreferences", QH, {"json": json.dumps({"auto_tmm_enabled": True})}, form=True, raw=True)
|
||||
c, prefs, _ = call("GET", QB + "/api/v2/app/preferences", QH)
|
||||
say(f" after : auto_tmm_enabled={prefs.get('auto_tmm_enabled')} (a category's save path applies only in automatic mode)")
|
||||
c, cats, _ = call("GET", QB + "/api/v2/torrents/categories", QH)
|
||||
for tag, d in DRIVES:
|
||||
M = f"/mnt/felhom-drives/{d}/userdata/media"
|
||||
for kind in ("tv", "movies"):
|
||||
name = f"{kind}-{tag}"
|
||||
form = {"category": name, "savePath": f"{M}/torrents/{kind}", "downloadPathEnabled": "true",
|
||||
"downloadPath": f"{M}/torrents/.temp"}
|
||||
ep = "editCategory" if name in (cats or {}) else "createCategory"
|
||||
call("POST", QB + f"/api/v2/torrents/{ep}", QH, form, form=True, raw=True)
|
||||
c, cats, _ = call("GET", QB + "/api/v2/torrents/categories", QH)
|
||||
for n, v in sorted((cats or {}).items()):
|
||||
say(f" category {n}: savePath={v.get('savePath')} downloadPath={v.get('download_path', v.get('downloadPath'))}")
|
||||
|
||||
# ---------------------------------------------------------------- Transmission
|
||||
TR = f"http://{ip('transmission')}:9091/transmission/rpc"
|
||||
tpw = env("transmission", "PASS")
|
||||
TA = "Basic " + base64.b64encode(f"admin:{tpw}".encode()).decode()
|
||||
SECRETS.append(TA.split()[1])
|
||||
say("== Transmission", TR, "(password: the deploy secret, read from the container's environment)")
|
||||
|
||||
|
||||
def trpc(method, args=None, host=None):
|
||||
h = {"Authorization": TA}
|
||||
if host:
|
||||
h["Host"] = host
|
||||
c, b, hd = call("POST", TR, h, {"method": method, "arguments": args or {}}, expect=(200, 409))
|
||||
if c == 409:
|
||||
h["X-Transmission-Session-Id"] = hd.get("x-transmission-session-id", "")
|
||||
c, b, hd = call("POST", TR, h, {"method": method, "arguments": args or {}}, expect=(200, 421))
|
||||
return c, b
|
||||
|
||||
|
||||
c, b = trpc("session-get", {"fields": ["download-dir", "incomplete-dir", "incomplete-dir-enabled", "rename-partial-files"]})
|
||||
say(f" before: {json.dumps((b or {}).get('arguments'))}")
|
||||
trpc("session-set", {"incomplete-dir-enabled": False})
|
||||
c, b = trpc("session-get", {"fields": ["download-dir", "incomplete-dir-enabled", "rename-partial-files"]})
|
||||
say(f" after : {json.dumps((b or {}).get('arguments'))} (one incomplete folder for all torrents would be on drive A)")
|
||||
# The probe the link code needs: can another container reach Transmission BY NAME? (Host-header allow-list)
|
||||
c, b = trpc("session-get", {"fields": ["version"]}, host="transmission:9091")
|
||||
say(f" probe Host: transmission:9091 -> HTTP {c} {redact(str(b))[:160]}")
|
||||
TR_BY_NAME = c == 200
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Sonarr / Radarr
|
||||
def arr(app, port, kind, root_kind):
|
||||
base = f"http://{ip(app)}:{port}/api/v3"
|
||||
H = {"X-Api-Key": apikey(app)}
|
||||
say(f"== {app} {base} (key: <ApiKey> in /config/config.xml, read from the config volume)")
|
||||
c, tags, _ = call("GET", base + "/tag", H)
|
||||
have = {t["label"]: t["id"] for t in tags or []}
|
||||
for tag, _d in DRIVES:
|
||||
if tag not in have:
|
||||
c, t, _ = call("POST", base + "/tag", H, {"label": tag})
|
||||
have[tag] = t["id"]
|
||||
say(f" tags: {have}")
|
||||
c, roots, _ = call("GET", base + "/rootfolder", H)
|
||||
rp = {r["path"].rstrip("/") for r in roots or []}
|
||||
for tag, d in DRIVES:
|
||||
p = f"/mnt/felhom-drives/{d}/userdata/media/{root_kind}"
|
||||
if p not in rp:
|
||||
call("POST", base + "/rootfolder", H, {"path": p})
|
||||
c, roots, _ = call("GET", base + "/rootfolder", H)
|
||||
for r in roots or []:
|
||||
say(f" root folder: {r['path']} accessible={r.get('accessible')} freeSpace={r.get('freeSpace')}")
|
||||
c, schema, _ = call("GET", base + "/downloadclient/schema", H)
|
||||
sch = {s["implementation"]: s for s in schema}
|
||||
c, clients, _ = call("GET", base + "/downloadclient", H)
|
||||
names = {x["name"] for x in clients or []}
|
||||
for tag, d in DRIVES:
|
||||
M = f"/mnt/felhom-drives/{d}/userdata/media"
|
||||
for impl, nm, vals in (
|
||||
("QBittorrent", f"qBittorrent {tag}", {"host": "qbittorrent", "port": 8080, "username": "admin",
|
||||
"password": qpw, f"{kind}Category": f"{'tv' if kind == 'tv' else 'movies'}-{tag}"}),
|
||||
("Transmission", f"Transmission {tag}", {"host": "transmission", "port": 9091, "username": "admin",
|
||||
"password": tpw, "urlBase": "/transmission/",
|
||||
# the schema's default category must be CLEARED: Sonarr/Radarr
|
||||
# refuse „Cannot use Category and Directory" (measured, run 1)
|
||||
f"{kind}Category": "",
|
||||
f"{kind}Directory": f"{M}/transmission/{root_kind}"})):
|
||||
if nm in names:
|
||||
say(f" client {nm}: exists — left as it is")
|
||||
continue
|
||||
body = json.loads(json.dumps(sch[impl]))
|
||||
body.update({"name": nm, "enable": True, "tags": [have[tag]], "priority": 1})
|
||||
for f in body["fields"]:
|
||||
if f["name"] in vals:
|
||||
f["value"] = vals[f["name"]]
|
||||
c, r, _ = call("POST", base + "/downloadclient", H, body, expect=(200, 201, 400))
|
||||
if c == 400:
|
||||
say(f" client {nm}: REFUSED by {app}'s own connection test: {redact(json.dumps(r))[:400]}")
|
||||
c, clients, _ = call("GET", base + "/downloadclient", H)
|
||||
for x in clients or []:
|
||||
f = {k["name"]: k.get("value") for k in x["fields"]}
|
||||
say(f" client {x['name']}: impl={x['implementation']} enable={x['enable']} tags={x['tags']} host={f.get('host')}"
|
||||
f" category={f.get(kind + 'Category')} directory={f.get(kind + 'Directory')}")
|
||||
c, mm, _ = call("GET", base + "/config/mediamanagement", H)
|
||||
say(f" before: minimumFreeSpaceWhenImporting={mm.get('minimumFreeSpaceWhenImporting')} MB, "
|
||||
f"copyUsingHardlinks={mm.get('copyUsingHardlinks')}")
|
||||
mm["minimumFreeSpaceWhenImporting"] = MIN_FREE_MB
|
||||
call("PUT", base + f"/config/mediamanagement/{mm['id']}", H, mm)
|
||||
c, mm, _ = call("GET", base + "/config/mediamanagement", H)
|
||||
say(f" after : minimumFreeSpaceWhenImporting={mm.get('minimumFreeSpaceWhenImporting')} MB, "
|
||||
f"copyUsingHardlinks={mm.get('copyUsingHardlinks')} (default kept)")
|
||||
|
||||
|
||||
arr("sonarr", 8989, "tv", "tv")
|
||||
arr("radarr", 7878, "movie", "movies")
|
||||
say("== done; Transmission reachable by its container name:", TR_BY_NAME)
|
||||
@@ -124,19 +124,23 @@ 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 — 9 rows (P2 1, P3 4, P4 4)
|
||||
## Apps & catalog — 13 rows (P2 2, P3 6, P4 5)
|
||||
|
||||
| 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-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-76** | Apps & catalog | P4 | **FileBrowser-created folders break the setgid chain, and a drop-zone's mode is not stable** **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-26, size S, roadmap state `idea (surfaced by the R-75 spike, 2026-07-26)`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** **2026-10-06 night: not a catalog fix** — the setgid chain is set by the controller's FileBrowser setup; a controller row. | — | Two related findings from `audits/SPIKE-catalog-data-paths-2026-07-26.md` P3/P5, both **pre-existing** and deliberately left alone by that spike. **(a)** FileBrowser Quantum 1.3.3 creates files `0644` and folders `0755` and does **not** propagate the setgid bit — even though the entrypoint wrapper's `umask 002` really is in effect (`/proc/1/status` `Umask: 0002`). Group inheritance itself works (a file uploaded into a 2775 group-100 dir landed group 100, not the process gid 1000), so the convention's *group* half holds and only its *mode* half is lost. The consequence is proven with a control: inside a UI-created `0755` folder a gid-1000 process's file landed group **1000**, while the identical write into the 2775 parent landed group **100**. So **any folder a customer creates through FileBrowser breaks the shared-group chain one level down.** Latent today — every userdata-touching catalog app that declares an identity declares uid/gid **1000**, the same uid FileBrowser runs as, so owner permissions mask it; it bites the day a content app runs as a different non-root uid with gid 1000. The comment at `infra/infra.go:156` is right that the image ignores `-e UMASK` but does not say t | CC |
|
||||
| **R-707** | Apps & catalog | P4 | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). **2026-09-29 (controller v0.280.0, catalog `d0e7e2e`):** every class-3 app fixed — mealie, wger, calibre-web by `after_install` (calibre-web with the new `password:24:special`), proven on 9202 fresh installs (`audits/login-gate-2026-09-29/D/`); the setup gate (decision 46, spike PASSED) built and live on immich, n8n, audiobookshelf (probes measured) and uptime-kuma (button) (`…/C/`); romm's and zipline's stale notes removed. **Left: 30 class-4 apps** — gate each (probe measured on 9202 where one exists — 11 upstream candidates listed in `…/B/B-VERDICT.md` §3; the button otherwise). **2026-09-29 afternoon (controller v0.281.0, catalog `6faf432`):** 28 more class-4 apps gated — 32 of 34 — each proven on 9202 (`audits/gate-rollout-2026-09-29/`B): stranger → gate page / 401, household reached the first-setup screen, the gate opened (9 by a measured probe, the rest by the press), the app answered after. seerr, outline, rallly: gated, their opening needs a media server / e-mail (not proven). **Left:** wanderer (R-714); plant-it is not installable. | **NARROWED** (2026-10-03 triage: the row's verdict was finished, but it names open work no other row carries — seerr, outline and rallly are gated, but the gate OPENING is not proven (needs a media server / e-mail)) — **CLOSED — 2026-09-29 (the rest → R-714)** | — | — | CC |
|
||||
| **R-905** | Apps & catalog | P4 | **wger's collected style files (283 MB) go into every wger backup, though the app rebuilds them at every start.** Since R-762's fix (`DJANGO_DEBUG=False` + `collectstatic`, catalog `cf1ed43`) the static files sit in a named volume that the backup legs copy like data (measured 283 MB, `audits/design-build-2026-10-06/`); `collectstatic` regenerates them, so they are rebuildable bytes in every Tier-1 unit, Tier-2 copy and off-site snapshot. wger is `lifecycle: hidden` and no box runs it (hub read 2026-10-08). Options to weigh: a non-backed-up volume class for regenerable data, or an anonymous volume. Found by the R-762 helpers 2026-10-08. | **OPEN — needs a design before wger is shown** | — | Design the „regenerable volume" exclusion (catalog + controller backup legs) | CC |
|
||||
| **R-995** | Apps & catalog | P4 | **Removing an app through the product can leave unnamed Docker volumes and an empty import folder behind.** MEASURED 2026-10-13 on demo-felhom after 61 removals (`documentation/audits/sizing-2026-10-13/METHOD.md` „Teardown”, `evidence/leftover-volumes-after-bench.txt`): 8 unnamed volumes (4–72 KB; immich 1, romm 2, privatebin 3, onlyoffice 2 — volumes the images declare themselves) and `userdata/import/{paperless,calibre,grimmory}` (empty, created by the deploy). The removal answered 200 and its own leftover check was clean, because it looks only at labelled volumes. No data risk; disk and tidiness only. Removed by name on the bench. | **READY** — filed 2026-10-13 | — | Make the removal account for the containers' unnamed volumes (and an empty import folder it created), with a test that deploys an image that declares a VOLUME | CC |
|
||||
| **R-1007** | Apps & catalog | P3 | **After a power cut Immich shows photos as done that are broken, and nothing repairs them: of 101 photos uploaded in the last ~30 s before the cut, 81 kept their original but got an EMPTY thumbnail and preview (162 zero-byte files), and 1 — answered „201 Created" 0.3 s before the cut — has no original at all.** Immich's job queues are empty after the start, so it never redoes them (`audits/night-power-2026-10-13/` R2, `immich-check.txt`). Same ext4 effect on a plain 40-byte file. Other apps may do the same with files written just before a cut. | **OPEN** — 2026-10-14 spike (`audits/power-cut-fixes-2026-10-14/README.md` F, Immich v3.2.4): „thumbnails: missing" does not fix a zero-byte thumbnail (nor after deleting it — Immich decides from its database); „thumbnails: all" (force) fixes them; `integrity-missing-files` (`/api/admin/integrity/report?type=missing_file`) finds a lost original; nothing restores one but a backup. **Operator ruling 2026-10-14: fix only the broken photos** — after an unclean boot, find the photos with an empty thumbnail or preview, rebuild only those, run the missing-files integrity check, and tell the operator about lost originals. No „thumbnails: all". **Decision 236 (operator 2026-10-14, after spike A `audits/immich-repair-2026-10-14/`): the box asks Immich through Immich's own job queue (no key; every user's photos; the lost-file report read from the database); the catalog pins the proven Immich versions.** 2026-10-14 B2 spike (`audits/power-cut-fixes-2026-10-14/README.md` B2): **yes** — `POST /api/assets/jobs {assetIds, name: regenerate-thumbnail}` rebuilt exactly the 10 broken assets (1.55 s, 5.3 s CPU) and rewrote none of the other 20; an empty file maps to its asset by the first 36 characters of its name. | — | CC: build per decision 236 — catalog block `after_unclean_boot` (Immich), controller runner after an unclean boot, household note for lost photos, operator-only event; live proof on demo-hp 9201 (`audits/immich-repair-2026-10-14/`) | 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)
|
||||
@@ -148,7 +152,7 @@ stopping line that lies.
|
||||
| **R-683** | App updates | P3 | **[P3-LOW] Watch: after a power cut during an update's health check, the hold named an HOUR-OLD second-drive copy, not the one the update's own backup should have just made.** 2026-09-24 chaos round 3 (nextcloud, `backup_max_age: 1m`): no `backing-up` phase was seen and the hold named Tier 2 at 13:04 for an update pressed at 14:04; the pre-cut controller log was lost with the container (the runner now saves it at arm time — R-320). Round 11, the same action without a power cut, named a fresh 14:34 copy and logged the Tier-2 copy. The sentence was TRUE (it named the copy it offered); the question is why the update did not back up first. Not reproduced; watch the next power-cut drill. `audits/night-2026-09-24/E/round-03*.json`, `E/round-11-controller-pre.log` | **OPEN — P3; owner: CC (watch)** | — | — | CC |
|
||||
| **R-785** | App updates | P3 | **[P3-LOW] SparkyFitness is pinned 11 releases and a major behind upstream (v0.17.3; upstream v1.7.3, v1.6.0 dated 2026-07-24).** READ 2026-10-01 (`audits/visitors-2026-10-01/C/bench/C1-previous-tag.txt`). **Needs:** an update walk 0.17 → 1.x through the ladder (bench + box), after R-784 is decided. | **OPEN — rank P3-LOW; owner: CC (after R-784)** | — | — | CC |
|
||||
|
||||
## Backup & restore — 29 rows (P2 3, P3 16, P4 10)
|
||||
## Backup & restore — 30 rows (P2 3, P3 17, P4 10)
|
||||
|
||||
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
@@ -171,6 +175,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-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 |
|
||||
@@ -278,11 +283,12 @@ stopping line that lies.
|
||||
| **R-989** | Business & legal | P4 | **Operator: read `documentation/audits/selfhst-survey-2026-10-12/FELHOM.md` §6 and rule on each recommendation.** READ-ONLY research 2026-10-12: the selfh.st self-hosting survey 2023–2025 (1,869 / 3,655 / 4,081 respondents, hobby users) against our catalog, features, pitch and hardware. Ten ranked recommendations, none acted on; the top three: a supported-hardware page (none exists — finding, §4), off-site backup as a *why* reason in the pitch (only 25.2 % of the crowd backs up off-site), Syncthing as the largest catalog gap (25.9 %). Tables: `TABLE.md`; method and limits: `METHOD.md`. | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages (the 2026 survey replaces it) | operator |
|
||||
| **R-990** | Business & legal | P4 | **Operator: read `documentation/audits/forum-research-2026-10-12/FELHOM.md` §5 and rule on each recommendation.** READ-ONLY research 2026-10-12: three years (#7943–#11092, 2,908 posts, 195 people, 82 beginner threads) of the public PROHARDVER! topic „Home server / házi szerver építése”, tagged and counted by person (`FINDINGS.md`, method and spot-check in `METHOD.md`; no username or post text in git). Ten ranked recommendations, none acted on; the top three: a „which machine works” page (8 of 33 beginners who name their hardware own only a Synology, a router disk or a Raspberry Pi — the installer cannot run there; the same gap is R-989's first recommendation from a different source, and the shipped-page half is R-991), „you do not choose an operating system — we do” (the biggest beginner question, 20 of 82 threads), and „bring your old office PC” as the cheap entry. Catalog gap (≥ 3 people): a DLNA server (7), an ad/DNS blocker (5), a camera recorder (5). | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages | operator |
|
||||
|
||||
## Process & tooling — 21 rows (P3 7, P4 14)
|
||||
## Process & tooling — 22 rows (P3 8, P4 14)
|
||||
|
||||
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **R-992** | Process & tooling | P3 | **The drill-catalog route that `09` §6.5 documents cannot be followed: `admin/app-catalog-drill` is PRIVATE and no box has catalog credentials.** Measured 2026-10-12 while publishing the Usenet pair: the box's clone fails with `fatal: could not read Username for 'https://gitea.dooplex.hu'` (exit 128), the Gitea API reports the drill repo `private=true` and the live catalog `private=false`, and NEITHER the current controller.yaml on 9202 NOR any of its 30 saved copies has ever carried `git.username` or `git.token` (the controller supports both — `internal/config/config.go` GitConfig). The route worked for the campaigns of 2026-10-10 and the torrent pair earlier the same day, so the repository's visibility changed between then and now. **Worked around, not fixed:** that campaign staged its two templates in the box's own catalog cache (`audits/usenet-2026-10-12/tools/stage_in_cache.py`), which is where the sync copies a catalog change anyway, with the sync interval raised to 12h for the duration and rescan used in place of sync. The real fix is an operator decision: either a read-only catalog credential on the scratch box (a secret on a Tier-0 guest) or making the drill repository readable. Until then every new-app campaign hits this. | WAITING-ON-OPERATOR | the operator's choice between a box credential and a readable drill repo | Decide; then either set `git.username`/`git.token` on 9202 from the credentials store, or flip the drill repo, and correct `09` §6.5 either way | operator |
|
||||
| **R-1017** | Process & tooling | P3 | **A comment names a guard test that does not exist.** `felhom-controller/controller/internal/appbackup/paths.go:58` says „`TestNoBareDrivePathToUserdataDir` is the guard that keeps the count from growing”; no test of that name (or `NoBareDrive*`) exists in the repo (grep 2026-10-14; positive control: `TestRemoveStack_R800_UserdataKeptIsNamed` is found). A comment asserting an invariant with no test pinning it — the class in the `felhom-testing` skill. | **READY** — filed 2026-10-14 | — | Write the guard (a test that fails when a bare drive path reaches `UserdataDir`), red-proof it, or correct the comment | CC |
|
||||
| **R-987** | Process & tooling | P4 | **The 53 catalog app images have never been scanned for known vulnerabilities.** Fix: a monthly trivy pass over the images the catalog pins, reported, not gating. | OPEN | — | Add to the monthly re-test | CC |
|
||||
| **R-986** | Process & tooling | P3 | **No dependency or secret scanner runs on any push, so a known-vulnerable library or a committed key is found only by a review like this one.** Proposed gate: govulncheck (called vulns only), gitleaks on the diff, trivy on our own images; start as WARN with an accepted-hits file, then FAIL. Tools and pinned versions: `audits/security-review-2026-10-12/SCANNERS.md`. Note: gitleaks' default rules did not find R-908's key. | OPEN | — | Build the gate, warn-first | CC |
|
||||
| **R-733** | Process & tooling | P3 | **[P3-LOW] The test bench has NO swap and the boxes have 512 MiB — so a box proof can pass on swap where the bench fails, and nobody records whether a customer guest has swap.** MEASURED 2026-09-30 (R-732): immich's first start was OOM-killed 61–104 times on the bench (swap 0) and passed on 9202 by swapping ~108 MB; the bench given 512 MiB swap passed too. demo-hp 9201, 9202 and demo-felhom 9201 all read `swap: 512`; the golden's guest config is not recorded in its bake evidence, so a customer guest's swap is NOT measured. The harness's memory watch judges `anon` against the limit and never reads `memory.swap.current`. **Needs:** the golden's `swap` read and recorded; the box walk and the harness report `memory.swap.peak` beside `anon`; a decision whether proofs run with swap off (the stricter venue, as R-732's fix was proven). | **READY — rank P3-LOW; owner: CC (harness + golden evidence)** **2026-10-06 night: NARROWED** — the harness records each container's `swap_peak` and the venue's swap (catalog branch `night-held-2026-10-06`, `3f4611c`; SwapRecorded red-proved; bench SwapTotal 0 kB, 9202 524288 kB). LEFT: the golden's swap in its bake evidence, the box walk's record, and whether proofs run with swap off. | — | — | CC |
|
||||
|
||||
Reference in New Issue
Block a user