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

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:
2026-10-14 17:36:37 +02:00
parent 70250ffb95
commit 8395aee4c2
41 changed files with 1866 additions and 3 deletions
@@ -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
@@ -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
@@ -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)