app guides night: R-1085 (the floor can still offer spoiled tag 0.316.3), R-1086 (qBittorrent password advice breaks the media wiring); pilot evidence and tools
gates / gates (push) Successful in 6m38s

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-15 22:37:08 +02:00
parent a3bf208242
commit bfd9b2a7ea
21 changed files with 1192 additions and 2 deletions
@@ -0,0 +1,49 @@
== taken: 2026-10-15T20:29:41Z guest 9201 (demo-hp), read-only
== docker ps (before = after: nothing installed, removed or restarted)
adventurelog ghcr.io/seanmorley15/adventurelog-backend:v0.13.0 Up 18 hours (healthy)
adventurelog-frontend ghcr.io/seanmorley15/adventurelog-frontend:v0.13.0 Up 18 hours (healthy)
adventurelog-postgres postgis/postgis:16-3.5-alpine Up 18 hours (healthy)
bentopdf ghcr.io/alam00000/bentopdf:v2.8.6 Up 18 hours (healthy)
bookstack lscr.io/linuxserver/bookstack:26.09.1 Up 18 hours (healthy)
bookstack-db mariadb:12.3 Up 18 hours (healthy)
calibre-web crocodilestick/calibre-web-automated:v4.0.8 Up 18 hours (healthy)
cloudflared cloudflare/cloudflared:2026.9.3 Up 29 hours (healthy)
docmost docmost/docmost:0.96.0 Up 18 hours (healthy)
docmost-postgres postgres:18-alpine Up 18 hours (healthy)
docmost-redis redis:7-alpine Up 18 hours (healthy)
felhom-controller gitea.dooplex.hu/admin/felhom-controller:0.317.0 Up 56 minutes (healthy)
filebrowser gtstef/filebrowser:1.5.6-stable Up 6 hours (healthy)
home-assistant ghcr.io/home-assistant/home-assistant:2026.9.4 Up 18 hours (healthy)
opengist ghcr.io/thomiceli/opengist:1.15 Up 18 hours (healthy)
paperless-postgres postgres:18-alpine Up 18 hours (healthy)
paperless-redis redis:7-alpine Up 18 hours (healthy)
paperless-webserver ghcr.io/paperless-ngx/paperless-ngx:2.20.15 Up 18 hours (healthy)
plex plexinc/pms-docker:1.41.4.9463-630c9f557 Up 12 hours (healthy)
privatebin privatebin/pdo:2.0.6 Up 18 hours (healthy)
traefik traefik:v3.7.13 Up 18 hours
== plex image
plexinc/pms-docker:1.41.4.9463-630c9f557
== plex mounts (FAQ: Plex only READS the drive)
/config rw=true
/media rw=false
/transcode rw=true
/mnt/felhom-drives/hdd_1/userdata/media rw=false
== plex env KEYS (FAQ: the box set the public address) - names only
ADVERTISE_IP
TZ
PLEX_CLAIM
== /identity (claimed flag)
claimed="1"
== controller log: media wiring lines for plex (positive observable: the box added folders / found nothing to add)
== VERDICT (read-only box check, Part A.3 under night rule 2)
- „Plex only reads the drive" (FAQ 2): PASS — /media and the per-drive media bind are rw=false.
- „The box set Plex up to answer on your own address" (FAQ 5): PASS — ADVERTISE_IP is in the container env (value not printed).
- Claimed state the guide assumes (sections 1 and 4, FAQ 6): PASS — /identity claimed="1".
- „The box adds a new drive's folders to your existing libraries and starts a scan" (section 2): NOT RE-READ TONIGHT.
The wiring runs on a media app's start; nothing started since the controller came up at 19:33 UTC, so the log holds
no wiring line, and an absent line is not evidence either way. Reading the libraries needs Plex's own token, which a
read-only night check does not take. The claim rests on the media build's live proof (G2 on Tester 1: a drive wired into every app, Plex included; G7 on this box: the live catalog)
— felhom.eu documentation/audits/media-build-2026-10-15/G and G7 — and on felhom-controller mediawire.go plex().
- Nothing installed, removed, restarted or changed (docker ps above; no POST sent).
@@ -0,0 +1,136 @@
{
"app": "plex",
"checked_version": "1.41.4.9463-630c9f557",
"hu": {
"sections": [
{
"title": "Nézés tévén vagy telefonon",
"intro": "A Plex appok a Plex fiókodon keresztül találják meg a szerveredet.",
"steps": [
"Nyisd meg a Plex appot, és jelentkezz be ugyanazzal a Plex fiókkal, amellyel a kódot kérted.",
"A „Kezdőképernyő” oldalon megjelennek a könyvtáraid.",
"Válassz egy filmet, és kattints a „Lejátszás” gombra."
]
},
{
"title": "Új filmek és új meghajtó",
"intro": "Amit a meghajtód media mappájába teszel, azt a Plex magától észreveszi.",
"steps": [
"A Radarr a media/movies, a Sonarr a media/tv mappába tesz mindent, a Plex innen olvas.",
"Új meghajtónál a doboz hozzáadja a meghajtó mappáit a meglévő film- és sorozatkönyvtáraidhoz, és elindít egy beolvasást.",
"Ha egy új fájl mégsem jelenik meg, a könyvtár menüjében kattints a „Könyvtárfájlok beolvasása” pontra."
]
},
{
"title": "Rossz cím vagy borító javítása",
"intro": "A Plex a fájl és a mappa nevéből találja ki, melyik film ez.",
"steps": [
"Nyisd meg a filmet, és kattints a „Több” menüre.",
"Válaszd a „Találatok pontosítása” pontot, és keresd meg a helyes címet.",
"Legközelebb segít, ha a mappa neve a film címe és éve, például: Nosferatu (1922)."
]
},
{
"title": "Megosztás a családdal",
"intro": "Minden családtag a saját Plex fiókjával nézhet.",
"steps": [
"A „Beállítások” oldalon nyisd meg a „Könyvtár hozzáférés kezelése” részt.",
"Válaszd ki, kivel és mely könyvtárakat osztod meg.",
"A családtag a saját Plex fiókjával lép be az appba."
]
}
],
"faq": [
{
"q": "Miért nem látom a filmet?",
"a": "Nézd meg, hogy a fájl a media/movies mappában van-e, és hogy a mappa neve a film címe és éve. Utána kattints a „Könyvtárfájlok beolvasása” pontra."
},
{
"q": "Törölhetek filmet a Plexből?",
"a": "Nem: a Plex csak olvassa a meghajtót. Törölni a fájlböngészőben, a Radarrban vagy a Sonarrban tudsz."
},
{
"q": "Kell Plex Pass?",
"a": "Otthoni nézéshez nem. Néhány funkcióhoz igen, például a bevezető átugrásához; ezt a Plex dönti el, nem a doboz."
},
{
"q": "Akadozik a lejátszás. Mit tehetek?",
"a": "A Plex ilyenkor átalakítja a videót, és ez a dobozon lassú lehet. Az appban a „Videó minőség” beállításnál válaszd az „Eredeti” értéket."
},
{
"q": "Elérem otthonon kívül?",
"a": "A doboz beállította, hogy a Plex a saját címeden is elérhető legyen. Otthonon kívüli nézéshez a Plex előfizetést kérhet; ezt a Plex szabja meg."
},
{
"q": "Lejárt a kód. Hol kérek újat?",
"a": "Ugyanott, ahol az elsőt: a címet az első lépések között találod. Az új kódot a vezérlőpulton, a Plex „Beállítások” fülén add meg."
}
]
},
"en": {
"sections": [
{
"title": "Watch on a TV or a phone",
"intro": "The Plex apps find your server through your Plex account.",
"steps": [
"Open the Plex app and sign in with the same Plex account you got the code with.",
"Your libraries appear on the „Home” page.",
"Choose a film and select „Play”."
]
},
{
"title": "New films and a new drive",
"intro": "Whatever you put into your drive's media folder, Plex notices by itself.",
"steps": [
"Radarr puts everything into media/movies and Sonarr into media/tv; Plex reads from there.",
"With a new drive, the box adds its folders to your existing film and series libraries and starts a scan.",
"If a new file still does not show up, select „Scan Library Files” in the library's menu."
]
},
{
"title": "Fix a wrong title or cover",
"intro": "Plex works out which film it is from the names of the file and the folder.",
"steps": [
"Open the film and select the „More” menu.",
"Choose „Fix Match” and search for the right title.",
"Next time it helps to name the folder after the film's title and year, for example: Nosferatu (1922)."
]
},
{
"title": "Share with the family",
"intro": "Every family member can watch with their own Plex account.",
"steps": [
"On the „Settings” page, open „Manage Library Access”.",
"Choose who gets which libraries.",
"The family member signs in to the app with their own Plex account."
]
}
],
"faq": [
{
"q": "Why can I not see the film?",
"a": "Check that the file is in the media/movies folder and that the folder is named after the film's title and year. Then select „Scan Library Files”."
},
{
"q": "Can I delete a film from Plex?",
"a": "No: Plex only reads the drive. You delete in the file browser, in Radarr or in Sonarr."
},
{
"q": "Do I need Plex Pass?",
"a": "Not for watching at home. Some features need it, for example skipping intros; Plex decides that, not the box."
},
{
"q": "Playback stutters. What can I do?",
"a": "Plex is converting the video, and that can be slow on the box. In the app, set „Video Quality” to „Original”."
},
{
"q": "Can I reach it away from home?",
"a": "The box set Plex up to answer on your own address. To watch away from home, Plex may ask for a subscription; Plex sets that rule."
},
{
"q": "The code ran out. Where do I get a new one?",
"a": "In the same place as the first one: its address is in the first steps. Enter the new code on the dashboard, on Plex's „Settings” tab."
}
]
}
}
@@ -0,0 +1,22 @@
app: plex checked against: 1.41.4.9463-630c9f557
corpora: plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json felhom-controller@123d2d5:controller/internal/i18n/locales/hu.json felhom-controller@123d2d5:controller/internal/i18n/locales/en.json
labels: 18 (Hungarian and English guide strings, every „…” occurrence)
OK [hu] „Kezdőképernyő” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:920
OK [hu] „Lejátszás” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1407
OK [hu] „Könyvtárfájlok beolvasása” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1693
OK [hu] „Több” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1172
OK [hu] „Találatok pontosítása” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:817
OK [hu] „Beállítások” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1355
OK [hu] „Könyvtár hozzáférés kezelése” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1111
OK [hu] „Videó minőség” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:2360
OK [hu] „Eredeti” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1359
OK [en] „Home” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:920
OK [en] „Play” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1407
OK [en] „Scan Library Files” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1693
OK [en] „More” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1172
OK [en] „Fix Match” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:817
OK [en] „Settings” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1774
OK [en] „Manage Library Access” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1111
OK [en] „Video Quality” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:2360
OK [en] „Original” plexinc/pms-docker:1.41.4.9463-630c9f557 image, Plex Web 4.143.0 (WebClient.bundle/Contents/Resources, copied from demo-hp 9201's running container):translations/hu.json:1359
RESULT: ALL FOUND — 18 label(s), 0 missing
@@ -0,0 +1,2 @@
BOX CHECK OWED — prowlarr was not running on demo-hp 9201 at 2026-10-15T20:29Z (docker ps in plex-box.txt), and night rule 2
allows no install there tonight. Label check only (§8 row „all found / not run"); checked_version set.
@@ -0,0 +1,92 @@
{
"app": "prowlarr",
"checked_version": "version-2.6.5.5623",
"hu": {
"sections": [
{"title": "Első forrás hozzáadása",
"intro": "A forrásokat (indexereket) csak itt adod meg, egyszer.",
"steps": [
"Az „Indexers” (Indexerek) oldalon kattints az „Add Indexer” (indexer hozzáadása) gombra.",
"Keresd meg a listában azt a forrást, amelyhez hozzáférésed van, és kattints rá.",
"Töltsd ki, amit a forrás kér, például a saját felhasználónevedet vagy kulcsodat.",
"Kattints a „Test” (próba) gombra, és ha sikeres, a „Save” (mentés) gombra."
]},
{"title": "A Sonarr és a Radarr megkapja",
"intro": "Ezt a doboz bekötötte, neked nincs vele teendőd.",
"steps": [
"A „Settings” (Beállítások) menü „Apps” (alkalmazások) oldalán látod a Sonarrt és a Radarrt.",
"Egy új forrás magától átkerül hozzájuk, mert a kapcsolat „Full Sync” (teljes szinkron) módban van.",
"Ha mégsem jelenik meg, az „Indexers” oldalon kattints a „Sync App Indexers” (indexerek átküldése) gombra."
]},
{"title": "Egy forrás kipróbálása",
"intro": "Letöltés nélkül is megnézheted, ad-e találatot egy forrás.",
"steps": [
"A „Search” (Keresés) oldalon írj be egy címet, és nézd meg a találatokat.",
"Az „Indexers” oldalon a „Test All Indexers” (összes kipróbálása) gomb minden forrást egyszerre ellenőriz."
]},
{"title": "Ha egy forrás hibás",
"intro": "A Prowlarr egy ideig kihagyja azt a forrást, amelyik többször hibázott, majd újra próbálja.",
"steps": [
"Nézd meg a „System” (Rendszer) menü „Status” (Állapot) lapját: a „Health” (állapot) rész sorolja a hibás forrásokat.",
"A „History” (Előzmények) oldalon látod, melyik keresés mit hozott.",
"Ha egy forrás véglegesen elromlott, nyisd meg az „Indexers” oldalon, és javítsd vagy töröld."
]}
],
"faq": [
{"q": "Melyik forrást válasszam?",
"a": "Ezt te döntöd el. A Felhom nem ad, nem ajánl és nem keres forrást. Csak olyat használj, amelyhez jogod van."},
{"q": "Kell a Sonarrban vagy a Radarrban is felvennem a forrásokat?",
"a": "Nem. A Prowlarr átküldi nekik, és ha itt módosítasz egy forrást, ott is módosul."},
{"q": "Mi a különbség a Prowlarr és a Sonarr között?",
"a": "A Prowlarr csak a forrásokat tartja nyilván. A Sonarr és a Radarr dönti el, mit keres és mit tölt le."},
{"q": "Mi történik, ha a Sonarrt később telepítem?",
"a": "A doboz akkor is összeköti őket: amikor a Sonarr vagy a Radarr elindul, megkapja a forrásokat, gombnyomás nélkül."},
{"q": "Mit mutat a „Stats” oldal?",
"a": "Forrásonként mutatja, hány keresésre és hány találatra jutott, és mennyi idő alatt válaszolt. Így látod, melyik forrás lassú."}
]
},
"en": {
"sections": [
{"title": "Add your first source",
"intro": "You add your sources (indexers) here only, and only once.",
"steps": [
"On the „Indexers” page, select „Add Indexer”.",
"Find the source you have access to in the list and select it.",
"Fill in what the source asks for, for example your own user name or key.",
"Select „Test”, and if it succeeds, select „Save”."
]},
{"title": "Sonarr and Radarr get them",
"intro": "The box connected this; there is nothing for you to do.",
"steps": [
"The „Apps” page of the „Settings” menu shows Sonarr and Radarr.",
"A new source reaches them by itself, because the link is set to „Full Sync”.",
"If one does not show up, select „Sync App Indexers” on the „Indexers” page."
]},
{"title": "Try a source",
"intro": "You can see whether a source finds anything without downloading.",
"steps": [
"On the „Search” page, type a title and look at the results.",
"On the „Indexers” page, „Test All Indexers” checks every source at once."
]},
{"title": "When a source fails",
"intro": "Prowlarr skips a source that failed several times for a while, then tries it again.",
"steps": [
"Look at the „Status” tab of the „System” menu: the „Health” part lists the failing sources.",
"The „History” page shows what each search brought back.",
"If a source is broken for good, open it on the „Indexers” page and fix it or delete it."
]}
],
"faq": [
{"q": "Which source should I choose?",
"a": "That is your decision. Felhom neither provides, suggests nor searches for a source. Use only one you have the right to use."},
{"q": "Do I also have to add the sources in Sonarr or Radarr?",
"a": "No. Prowlarr sends them over, and when you change a source here, it changes there too."},
{"q": "What is the difference between Prowlarr and Sonarr?",
"a": "Prowlarr only keeps the list of sources. Sonarr and Radarr decide what to search for and what to download."},
{"q": "What happens if I install Sonarr later?",
"a": "The box still connects them: when Sonarr or Radarr starts, it gets the sources, with no button to press."},
{"q": "What does the „Stats” page show?",
"a": "For each source: how many searches and results it had, and how long it took to answer. That shows you which source is slow."}
]
}
}
@@ -0,0 +1,34 @@
app: prowlarr checked against: version-2.6.5.5623
corpora: Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json Prowlarr/Prowlarr@v2.6.5.5623(12c3278):frontend/src felhom-controller@123d2d5:controller/internal/i18n/locales/hu.json felhom-controller@123d2d5:controller/internal/i18n/locales/en.json
labels: 30 (Hungarian and English guide strings, every „…” occurrence)
OK [hu] „Indexers” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:456
OK [hu] „Add Indexer” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:17
OK [hu] „Test” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:731
OK [hu] „Save” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:653
OK [hu] „Settings” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:682
OK [hu] „Apps” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:75
OK [hu] „Full Sync” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:306
OK [hu] „Sync App Indexers” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:714
OK [hu] „Search” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:659
OK [hu] „Test All Indexers” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:735
OK [hu] „System” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:720
OK [hu] „Status” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:710
OK [hu] „Health” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:317
OK [hu] „History” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:320
OK [hu] „Stats” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:709
OK [en] „Indexers” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:456
OK [en] „Add Indexer” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:17
OK [en] „Test” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:731
OK [en] „Save” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:653
OK [en] „Apps” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:75
OK [en] „Settings” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:682
OK [en] „Full Sync” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:306
OK [en] „Sync App Indexers” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:714
OK [en] „Search” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:659
OK [en] „Test All Indexers” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:735
OK [en] „Status” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:710
OK [en] „System” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:720
OK [en] „Health” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:317
OK [en] „History” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:320
OK [en] „Stats” Prowlarr/Prowlarr@v2.6.5.5623(12c3278):src/NzbDrone.Core/Localization/Core/en.json:709
RESULT: ALL FOUND — 30 label(s), 0 missing
@@ -0,0 +1,2 @@
BOX CHECK OWED — qbittorrent was not running on demo-hp 9201 at 2026-10-15T20:29Z (docker ps in plex-box.txt), and night rule 2
allows no install there tonight. Label check only (§8 row „all found / not run"); checked_version set.
@@ -0,0 +1,94 @@
{
"app": "qbittorrent",
"checked_version": "5.2.4-1",
"hu": {
"sections": [
{"title": "Torrent hozzáadása",
"intro": "Csak olyan torrentet adj hozzá, amelynek a tartalmához jogod van, például egy Linux telepítőét a kiadó saját oldaláról.",
"steps": [
"Kattints a felső sávban az „Add Torrent Link...” (torrent hozzáadása linkkel) gombra.",
"Illeszd be a torrent vagy a magnet linkjét, és kattints a „Download” (Letöltés) gombra.",
"Ha torrentfájlod van, használd az „Add Torrent File...” (torrentfájl hozzáadása) gombot."
]},
{"title": "Leállítás és törlés",
"intro": "Egy torrentet bármikor megállíthatsz vagy eltávolíthatsz.",
"steps": [
"Jelöld ki a torrentet, és kattints a „Stop” (leállítás) gombra. A „Start” (indítás) folytatja.",
"Eltávolításhoz kattints a „Remove” (eltávolítás) gombra.",
"Ha a letöltött fájlokat is törölnéd, jelöld be az „Also remove the content files” (a fájlok törlése is) jelölőt."
]},
{"title": "Lassabb letöltés napközben",
"intro": "Beállíthatod, hogy napközben kevésbé terhelje a netedet.",
"steps": [
"Kattints az „Options” (beállítások) gombra, majd a „Speed” (sebesség) lapra.",
"Az „Alternative Rate Limits” (másodlagos korlát) részben adj meg alacsonyabb értékeket.",
"Jelöld be a „Schedule the use of alternative rate limits” (ütemezés) jelölőt, és add meg, mikor legyen érvényes.",
"Mentsd el a beállításokat a lap alján."
]},
{"title": "Meddig ossza tovább?",
"intro": "A kész letöltést a qBittorrent tovább küldi másoknak. Ezt korlátozhatod.",
"steps": [
"Az „Options” (beállítások) „BitTorrent” lapján keresd a „Seeding Limits” (megosztási korlát) részt.",
"A „When ratio reaches” (ha az arány eléri) jelölővel megadhatod, hányszoros feltöltés után álljon le.",
"Mentsd el a beállításokat a lap alján."
]}
],
"faq": [
{"q": "Hová kerülnek a letöltések?",
"a": "A meghajtódra, a media/torrents mappába. A fájlböngészőben is látod őket."},
{"q": "Be kell állítanom a Sonarr és a Radarr kategóriáit?",
"a": "Nem. A doboz minden meghajtóhoz létrehozta a tv- és a movies- kezdetű kategóriákat, és bekapcsolta az automatikus kezelést. Ezeket ne töröld és ne nevezd át."},
{"q": "Ha eltávolítom a torrentet, eltűnik a film a könyvtárból is?",
"a": "Nem. A Sonarr és a Radarr ugyanazt a fájlt egy második néven tette a könyvtárba, ez a torrent eltávolítása után is megmarad."},
{"q": "Mit jelent a „Seeding” állapot?",
"a": "A letöltés kész, és a qBittorrent most másoknak küldi tovább. Ezt a „Stop” gombbal leállíthatod."},
{"q": "Megváltoztathatom, hová mentsen?",
"a": "Ne tedd. A qBittorrent csak a media/torrents mappát látja a meghajtódon, máshová mentett fájl nem a meghajtóra kerül, és a Sonarr meg a Radarr sem találja."}
]
},
"en": {
"sections": [
{"title": "Add a torrent",
"intro": "Add only a torrent whose content you have the right to, for example a Linux installer from its publisher's own site.",
"steps": [
"In the bar at the top, select „Add Torrent Link...”.",
"Paste the torrent or magnet link and select „Download”.",
"If you have a torrent file, use „Add Torrent File...”."
]},
{"title": "Stop and remove",
"intro": "You can stop or remove a torrent at any time.",
"steps": [
"Select the torrent and select „Stop”. „Start” carries on.",
"To remove it, select „Remove”.",
"To delete the downloaded files as well, tick „Also remove the content files”."
]},
{"title": "Slower downloads by day",
"intro": "You can make it use less of your internet line during the day.",
"steps": [
"Select „Options”, then the „Speed” tab.",
"Under „Alternative Rate Limits”, enter lower values.",
"Tick „Schedule the use of alternative rate limits” and set when it applies.",
"Save the settings at the bottom of the page."
]},
{"title": "How long does it share?",
"intro": "qBittorrent keeps sending a finished download to others. You can limit that.",
"steps": [
"On the „BitTorrent” tab of „Options”, find „Seeding Limits”.",
"With „When ratio reaches”, set after how many times the size it stops uploading.",
"Save the settings at the bottom of the page."
]}
],
"faq": [
{"q": "Where do the downloads go?",
"a": "To your drive, into the media/torrents folder. You see them in the file browser too."},
{"q": "Do I have to set up the categories for Sonarr and Radarr?",
"a": "No. For every drive, the box created the categories that start with tv- and movies-, and switched on automatic management. Do not delete or rename them."},
{"q": "If I remove the torrent, does the film leave the library too?",
"a": "No. Sonarr and Radarr put the same file into the library under a second name, and that stays after the torrent is removed."},
{"q": "What does „Seeding” mean?",
"a": "The download is finished and qBittorrent is now sending it on to others. „Stop” ends that."},
{"q": "Can I change where it saves?",
"a": "Do not. qBittorrent sees only the media/torrents folder on your drive; a file saved anywhere else is not on the drive, and Sonarr and Radarr cannot find it."}
]
}
}
@@ -0,0 +1,34 @@
app: qbittorrent checked against: 5.2.4-1
corpora: qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/public felhom-controller@123d2d5:controller/internal/i18n/locales/hu.json felhom-controller@123d2d5:controller/internal/i18n/locales/en.json
labels: 30 (Hungarian and English guide strings, every „…” occurrence)
OK [hu] „Add Torrent Link...” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/index.html:70
OK [hu] „Download” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/addtorrent.html:401
OK [hu] „Add Torrent File...” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/index.html:64
OK [hu] „Stop” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/shareratio.html:201
OK [hu] „Start” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/index.html:78
OK [hu] „Remove” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/shareratio.html:202
OK [hu] „Also remove the content files” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/confirmdeletion.html:11
OK [hu] „Options” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/index.html:140
OK [hu] „Speed” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferencesToolbar.html:14
OK [hu] „Alternative Rate Limits” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferences.html:643
OK [hu] „Schedule the use of alternative rate limits” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferences.html:664
OK [hu] „BitTorrent” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferencesToolbar.html:17
OK [hu] „Seeding Limits” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferences.html:811
OK [hu] „When ratio reaches” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferences.html:817
OK [hu] „Seeding” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/filters.html:8
OK [en] „Add Torrent Link...” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/index.html:70
OK [en] „Download” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/addtorrent.html:401
OK [en] „Add Torrent File...” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/index.html:64
OK [en] „Stop” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/shareratio.html:201
OK [en] „Start” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/index.html:78
OK [en] „Remove” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/shareratio.html:202
OK [en] „Also remove the content files” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/confirmdeletion.html:11
OK [en] „Options” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/index.html:140
OK [en] „Speed” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferencesToolbar.html:14
OK [en] „Alternative Rate Limits” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferences.html:643
OK [en] „Schedule the use of alternative rate limits” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferences.html:664
OK [en] „BitTorrent” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferencesToolbar.html:17
OK [en] „Seeding Limits” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferences.html:811
OK [en] „When ratio reaches” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/preferences.html:817
OK [en] „Seeding” qbittorrent/qBittorrent@release-5.2.4(a1e4649):src/webui/www/private/views/filters.html:8
RESULT: ALL FOUND — 30 label(s), 0 missing
@@ -0,0 +1,2 @@
BOX CHECK OWED — radarr was not running on demo-hp 9201 at 2026-10-15T20:29Z (docker ps in plex-box.txt), and night rule 2
allows no install there tonight. Label check only (§8 row „all found / not run"); checked_version set.
@@ -0,0 +1,100 @@
{
"app": "radarr",
"checked_version": "6.4.4",
"hu": {
"sections": [
{"title": "Első film hozzáadása",
"intro": "A Radarr megkeresi és letölti a filmet, amint elérhető.",
"steps": [
"A bal oldali menüben a „Movies” (Filmek) alatt kattints az „Add New” (Új hozzáadása) pontra.",
"Írd be a film címét, és kattints a megfelelő találatra.",
"A „Root Folder” (gyökérmappa) mezőben válaszd annak a meghajtónak a media/movies mappáját, ahová a filmet szeretnéd.",
"A „Tags” (címkék) mezőbe írd ugyanannak a meghajtónak a címkéjét, például hdd-1. Címke nélkül a film nem kap letöltőt.",
"Hagyd bejelölve a „Start search for missing movie” (film keresése) jelölőt, és kattints az „Add Movie” (Film hozzáadása) gombra."
]},
{"title": "Mikor keresi a filmet?",
"intro": "A Radarr csak akkor keres, amikor a film már elérhető lehet.",
"steps": [
"A film hozzáadásakor a „Minimum Availability” (legkorábbi elérhetőség) mezőben állítod, mikor kezdjen keresni.",
"Az „Announced” (bejelentve) és az „In Cinemas” (moziban) korán keres, gyakran eredmény nélkül.",
"A „Released” (megjelent) a legbiztosabb: akkor keres, amikor a filmet már kiadták otthoni nézésre."
]},
{"title": "A letöltés követése",
"intro": "A letöltés a háttérben fut, az oldalt be is zárhatod.",
"steps": [
"Az „Activity” (Tevékenység) menü „Queue” (Sor) lapján látod, mi töltődik éppen.",
"Ha a film letöltődött, a Radarr magától átteszi a film mappájába. A Plex és a Jellyfin innen látja.",
"A „Calendar” (Naptár) mutatja, mikor jelenik meg egy figyelt film."
]},
{"title": "Ha valami nem működik",
"intro": "A Radarr maga jelzi, ha valami hibás.",
"steps": [
"Nézd meg a „System” (Rendszer) menü „Status” (Állapot) lapját: a „Health” (állapot) rész sorolja a hibákat.",
"A „Settings” (Beállítások) menü „Download Clients” (letöltő kliensek) oldalán látod a letöltőket, amelyeket a doboz bekötött.",
"Ha nincs találat, nézd meg a Prowlarr „Indexers” (indexerek) oldalát: legalább egy forrás kell."
]}
],
"faq": [
{"q": "Honnan tölt le a Radarr?",
"a": "Azokról a forrásokról (indexerekről), amelyeket te adsz meg a Prowlarrban. A doboz összeköti a kettőt, de forrást nem ad és nem ajánl: ez a te döntésed."},
{"q": "Be kell állítanom a letöltő klienst és a mappákat?",
"a": "Nem. A doboz minden meghajtódhoz bekötötte a telepített letöltőket (qBittorrent, Transmission), a gyökérmappát és a címkét. Neked csak a filmet kell hozzáadnod."},
{"q": "Miért van ugyanaz a fájl a torrents és a movies mappában is?",
"a": "Nem foglal dupla helyet: ugyanaz a fájl két néven (hardlink). A letöltő tovább oszthatja, a könyvtárad pedig rendezett marad."},
{"q": "Letöltötte, de nem került a könyvtárba. Miért?",
"a": "Ha a meghajtón kevesebb mint 10 GB maradna, a Radarr nem importál. Ezt a doboz állította be: „Settings”, „Media Management”, „Minimum Free Space” (legkevesebb szabad hely). Szabadíts fel helyet, és a következő ellenőrzéskor folytatja."},
{"q": "Ajánl filmeket?",
"a": "Igen: a „Discover” (Felfedezés) oldal a meglévő filmjeid alapján mutat ötleteket, és onnan egy kattintással hozzáadhatod őket."},
{"q": "Mit tölthetek le?",
"a": "Csak olyan tartalmat, amihez jogod van, például közkincs (public domain) filmeket."}
]
},
"en": {
"sections": [
{"title": "Add your first film",
"intro": "Radarr finds and downloads a film as soon as it is available.",
"steps": [
"In the menu on the left, under „Movies”, select „Add New”.",
"Type the title of the film and select the right result.",
"In „Root Folder”, choose the media/movies folder of the drive you want the film on.",
"In „Tags”, type the tag of that same drive, for example hdd-1. Without a tag the film gets no download client.",
"Leave „Start search for missing movie” ticked and select „Add Movie”."
]},
{"title": "When does it search?",
"intro": "Radarr searches only once a film may be available.",
"steps": [
"When you add a film, „Minimum Availability” sets when the search starts.",
"„Announced” and „In Cinemas” search early, and often find nothing.",
"„Released” is the safest: it searches once the film is out for home viewing."
]},
{"title": "Follow a download",
"intro": "Downloads run in the background, so you can close the page.",
"steps": [
"The „Queue” tab of the „Activity” menu shows what is downloading now.",
"When a film is done, Radarr moves it into the film's folder by itself. Plex and Jellyfin see it there.",
"The „Calendar” shows when a film you watch for comes out."
]},
{"title": "When something does not work",
"intro": "Radarr tells you itself when something is wrong.",
"steps": [
"Look at the „Status” tab of the „System” menu: the „Health” part lists the problems.",
"The „Download Clients” page of the „Settings” menu shows the download clients the box connected.",
"If nothing is found, look at the „Indexers” page in Prowlarr: you need at least one source."
]}
],
"faq": [
{"q": "Where does Radarr download from?",
"a": "From the sources (indexers) you add in Prowlarr. The box connects the two, but it neither provides nor suggests a source: that is your choice."},
{"q": "Do I have to set up the download client and the folders?",
"a": "No. For every drive, the box connected the installed download clients (qBittorrent, Transmission), the root folder and the tag. You only add the film."},
{"q": "Why is the same file in both the torrents and the movies folder?",
"a": "It does not take double space: it is one file with two names (a hardlink). The download client can keep sharing it, and your library stays tidy."},
{"q": "It downloaded, but it is not in the library. Why?",
"a": "Radarr does not import if less than 10 GB would be left on the drive. The box set this: „Settings”, „Media Management”, „Minimum Free Space”. Free up some space and it goes on at the next check."},
{"q": "Does it suggest films?",
"a": "Yes: the „Discover” page shows ideas based on the films you have, and you can add one from there with a click."},
{"q": "What may I download?",
"a": "Only content you have the right to, for example films in the public domain."}
]
}
}
@@ -0,0 +1,48 @@
app: radarr checked against: 6.4.4
corpora: Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json Radarr/Radarr@v6.4.4.10685(a96bf7c):frontend/src felhom-controller@123d2d5:controller/internal/i18n/locales/hu.json felhom-controller@123d2d5:controller/internal/i18n/locales/en.json
labels: 44 (Hungarian and English guide strings, every „…” occurrence)
OK [hu] „Movies” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1188
OK [hu] „Add New” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:35
OK [hu] „Root Folder” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:132
OK [hu] „Tags” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1902
OK [hu] „Start search for missing movie” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1868
OK [hu] „Add Movie” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:33
OK [hu] „Minimum Availability” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1092
OK [hu] „Announced” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:78
OK [hu] „In Cinemas” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:867
OK [hu] „Released” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1613
OK [hu] „Activity” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:4
OK [hu] „Queue” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1559
OK [hu] „Calendar” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:181
OK [hu] „System” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1886
OK [hu] „Status” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:133
OK [hu] „Health” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:748
OK [hu] „Settings” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1786
OK [hu] „Download Clients” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:570
OK [hu] „Indexers” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:942
OK [hu] „Media Management” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1062
OK [hu] „Minimum Free Space” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1097
OK [hu] „Discover” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:410
OK [en] „Movies” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1188
OK [en] „Add New” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:35
OK [en] „Root Folder” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:132
OK [en] „Tags” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1902
OK [en] „Start search for missing movie” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1868
OK [en] „Add Movie” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:33
OK [en] „Minimum Availability” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1092
OK [en] „Announced” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:78
OK [en] „In Cinemas” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:867
OK [en] „Released” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1613
OK [en] „Queue” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1559
OK [en] „Activity” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:4
OK [en] „Calendar” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:181
OK [en] „Status” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:133
OK [en] „System” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1886
OK [en] „Health” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:748
OK [en] „Download Clients” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:570
OK [en] „Settings” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1786
OK [en] „Indexers” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:942
OK [en] „Media Management” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1062
OK [en] „Minimum Free Space” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:1097
OK [en] „Discover” Radarr/Radarr@v6.4.4.10685(a96bf7c):src/NzbDrone.Core/Localization/Core/en.json:410
RESULT: ALL FOUND — 44 label(s), 0 missing
@@ -0,0 +1,2 @@
BOX CHECK OWED — sonarr was not running on demo-hp 9201 at 2026-10-15T20:29Z (docker ps in plex-box.txt), and night rule 2
allows no install there tonight. Label check only (§8 row „all found / not run"); checked_version set.
@@ -0,0 +1,100 @@
{
"app": "sonarr",
"checked_version": "4.0.20",
"hu": {
"sections": [
{"title": "Első sorozat hozzáadása",
"intro": "A Sonarr figyeli a sorozatot, és letölti az új részeket, amint megjelennek.",
"steps": [
"A bal oldali menüben a „Series” (Sorozatok) alatt kattints az „Add New” (Új hozzáadása) pontra.",
"Írd be a sorozat címét, és kattints a megfelelő találatra.",
"A „Root Folder” (gyökérmappa) mezőben válaszd annak a meghajtónak a media/tv mappáját, ahová a sorozatot szeretnéd.",
"A „Tags” (címkék) mezőbe írd ugyanannak a meghajtónak a címkéjét, például hdd-1. Címke nélkül a sorozat nem kap letöltőt.",
"Hagyd bejelölve a „Start search for missing episodes” (hiányzó részek keresése) jelölőt, és add hozzá a sorozatot."
]},
{"title": "A letöltés követése",
"intro": "A letöltés a háttérben fut, az oldalt be is zárhatod.",
"steps": [
"Az „Activity” (Tevékenység) menü „Queue” (Sor) lapján látod, mi töltődik éppen.",
"Ha egy rész letöltődött, a Sonarr magától átteszi a sorozat mappájába. A Plex és a Jellyfin innen látja.",
"A „Calendar” (Naptár) mutatja, mikor jön a következő rész."
]},
{"title": "Hiányzó részek keresése",
"intro": "Ha egy régebbi évad hiányzik, egyszerre kerestetheted.",
"steps": [
"Nyisd meg a „Wanted” (Keresett) menü „Missing” (Hiányzó) lapját.",
"Jelöld be a kívánt részeket, és kattints a „Search Selected” (kijelöltek keresése) gombra.",
"Ha mindet keresnéd, használd a „Search All” (összes keresése) gombot."
]},
{"title": "Ha valami nem működik",
"intro": "A Sonarr maga jelzi, ha valami hibás.",
"steps": [
"Nézd meg a „System” (Rendszer) menü „Status” (Állapot) lapját: a „Health” (állapot) rész sorolja a hibákat.",
"A „Settings” (Beállítások) menü „Download Clients” (letöltő kliensek) oldalán látod a letöltőket, amelyeket a doboz bekötött.",
"Ha nincs találat, nézd meg a Prowlarr „Indexers” (indexerek) oldalát: legalább egy forrás kell."
]}
],
"faq": [
{"q": "Honnan tölt le a Sonarr?",
"a": "Azokról a forrásokról (indexerekről), amelyeket te adsz meg a Prowlarrban. A doboz összeköti a kettőt, de forrást nem ad és nem ajánl: ez a te döntésed."},
{"q": "Be kell állítanom a letöltő klienst és a mappákat?",
"a": "Nem. A doboz minden meghajtódhoz bekötötte a telepített letöltőket (qBittorrent, Transmission), a gyökérmappát és a címkét. Neked csak a sorozatot kell hozzáadnod."},
{"q": "Miért van ugyanaz a fájl a torrents és a tv mappában is?",
"a": "Nem foglal dupla helyet: ugyanaz a fájl két néven (hardlink). A letöltő tovább oszthatja, a könyvtárad pedig rendezett marad."},
{"q": "Letöltötte, de nem került a könyvtárba. Miért?",
"a": "Ha a meghajtón kevesebb mint 10 GB maradna, a Sonarr nem importál. Ezt a doboz állította be: „Settings”, „Media Management”, „Minimum Free Space” (legkevesebb szabad hely). Szabadíts fel helyet, és a következő ellenőrzéskor folytatja."},
{"q": "Hol nézhetem meg a sorozatokat?",
"a": "A Plexben vagy a Jellyfinben. A Sonarr csak letölt és rendez, lejátszani nem tud."},
{"q": "Mit tölthetek le?",
"a": "Csak olyan tartalmat, amihez jogod van, például szabad licencű műsorokat."}
]
},
"en": {
"sections": [
{"title": "Add your first series",
"intro": "Sonarr watches a series and downloads each new episode as it comes out.",
"steps": [
"In the menu on the left, under „Series”, select „Add New”.",
"Type the title of the series and select the right result.",
"In „Root Folder”, choose the media/tv folder of the drive you want the series on.",
"In „Tags”, type the tag of that same drive, for example hdd-1. Without a tag the series gets no download client.",
"Leave „Start search for missing episodes” ticked and add the series."
]},
{"title": "Follow a download",
"intro": "Downloads run in the background, so you can close the page.",
"steps": [
"The „Queue” tab of the „Activity” menu shows what is downloading now.",
"When an episode is done, Sonarr moves it into the series folder by itself. Plex and Jellyfin see it there.",
"The „Calendar” shows when the next episode is due."
]},
{"title": "Search for missing episodes",
"intro": "If an older season is missing, you can search for it in one go.",
"steps": [
"Open the „Missing” tab of the „Wanted” menu.",
"Tick the episodes you want and select „Search Selected”.",
"To search for all of them, use „Search All”."
]},
{"title": "When something does not work",
"intro": "Sonarr tells you itself when something is wrong.",
"steps": [
"Look at the „Status” tab of the „System” menu: the „Health” part lists the problems.",
"The „Download Clients” page of the „Settings” menu shows the download clients the box connected.",
"If nothing is found, look at the „Indexers” page in Prowlarr: you need at least one source."
]}
],
"faq": [
{"q": "Where does Sonarr download from?",
"a": "From the sources (indexers) you add in Prowlarr. The box connects the two, but it neither provides nor suggests a source: that is your choice."},
{"q": "Do I have to set up the download client and the folders?",
"a": "No. For every drive, the box connected the installed download clients (qBittorrent, Transmission), the root folder and the tag. You only add the series."},
{"q": "Why is the same file in both the torrents and the tv folder?",
"a": "It does not take double space: it is one file with two names (a hardlink). The download client can keep sharing it, and your library stays tidy."},
{"q": "It downloaded, but it is not in the library. Why?",
"a": "Sonarr does not import if less than 10 GB would be left on the drive. The box set this: „Settings”, „Media Management”, „Minimum Free Space”. Free up some space and it goes on at the next check."},
{"q": "Where do I watch the series?",
"a": "In Plex or Jellyfin. Sonarr only downloads and files; it cannot play anything."},
{"q": "What may I download?",
"a": "Only content you have the right to, for example shows under a free licence."}
]
}
}
@@ -0,0 +1,44 @@
app: sonarr checked against: 4.0.20
corpora: Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json Sonarr/Sonarr@v4.0.20.3014(cab419a):frontend/src felhom-controller@123d2d5:controller/internal/i18n/locales/hu.json felhom-controller@123d2d5:controller/internal/i18n/locales/en.json
labels: 40 (Hungarian and English guide strings, every „…” occurrence)
OK [hu] „Series” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1860
OK [hu] „Add New” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:38
OK [hu] „Root Folder” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:139
OK [hu] „Tags” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1998
OK [hu] „Start search for missing episodes” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:45
OK [hu] „Activity” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:7
OK [hu] „Queue” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1631
OK [hu] „Calendar” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:184
OK [hu] „Wanted” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:2139
OK [hu] „Missing” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1207
OK [hu] „Search Selected” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1819
OK [hu] „Search All” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1806
OK [hu] „System” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1984
OK [hu] „Status” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:141
OK [hu] „Health” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:781
OK [hu] „Settings” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1901
OK [hu] „Download Clients” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:571
OK [hu] „Indexers” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1043
OK [hu] „Media Management” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1159
OK [hu] „Minimum Free Space” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1201
OK [en] „Series” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1860
OK [en] „Add New” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:38
OK [en] „Root Folder” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:139
OK [en] „Tags” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1998
OK [en] „Start search for missing episodes” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:45
OK [en] „Queue” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1631
OK [en] „Activity” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:7
OK [en] „Calendar” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:184
OK [en] „Missing” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1207
OK [en] „Wanted” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:2139
OK [en] „Search Selected” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1819
OK [en] „Search All” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1806
OK [en] „Status” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:141
OK [en] „System” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1984
OK [en] „Health” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:781
OK [en] „Download Clients” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:571
OK [en] „Settings” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1901
OK [en] „Indexers” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1043
OK [en] „Media Management” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1159
OK [en] „Minimum Free Space” Sonarr/Sonarr@v4.0.20.3014(cab419a):src/NzbDrone.Core/Localization/Core/en.json:1201
RESULT: ALL FOUND — 40 label(s), 0 missing
@@ -0,0 +1,36 @@
import io, os, sys, subprocess, shutil
ROOT = "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu"
sys.path.insert(0, os.path.join(ROOT, "scripts"))
import test_gate_decoys as T
CG = os.path.join(ROOT, "scripts", "check-copy-i18n.py")
GV = os.path.join(ROOT, "scripts", "check-guide-version.py")
P = [
("R1 guide strings counted as copy", CG, ' g = ai.get("guide") if isinstance(ai.get("guide"), dict) else {}', ' g = {}'),
("R2 overlay accepts guide", CG, '"prerequisites",\n "guide"):', '"prerequisites"):'),
("R3 checked_version refused in overlay", CG, ' if k == "checked_version":', ' if False:'),
("R4 empty steps refused", CG, ' if not isinstance(steps, list) or not steps:', ' if not isinstance(steps, list):'),
("R5 faq answer required", CG, ' if not (isinstance(qa.get(k), str) and qa.get(k).strip()):', ' if not isinstance(qa.get(k), (str, type(None))):'),
("R6 quoted version", CG, ' if cv is not None and not isinstance(cv, str):', ' if False:'),
("R7 section/faq counts", CG, ' if len(lst) != len(hu_lst):', ' if len(lst) < len(hu_lst):'),
("R8 step counts", CG, ' if len(st) != hu_n:', ' if len(st) > hu_n + 99:'),
("R9 inert English guide", CG, ' if not isinstance(hu_g, dict):\n errs.append', ' if False:\n errs.append'),
("R10 source deny-list", CG, ' for name in SOURCE_DENYLIST:', ' for name in []:'),
("R11 WARN on a moved image", GV, ' if cv in cands:', ' if cv:'),
("R12 the app service, not a sidecar", GV, ' cands = [tags[s] for s in main_svcs] or list(tags.values())', ' cands = list(tags.values())'),
]
for name, path, old, new in P:
src = io.open(path, encoding="utf-8").read()
assert src.count(old) == 1, name
io.open(path, "w", encoding="utf-8").write(src.replace(old, new))
try:
T.fails[:] = []
clone = T.make_clone()
import contextlib
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
T.guide_cases(clone)
shutil.rmtree(clone, ignore_errors=True)
names = [f.split(":")[0] + ":" + f.split(":")[1] for f in T.fails]
print("%-42s -> %d case(s) FAIL: %s" % (name, len(T.fails), "; ".join(n.strip() for n in names)))
finally:
io.open(path, "w", encoding="utf-8").write(src)
@@ -0,0 +1,12 @@
R1 guide strings counted as copy -> 3 case(s) FAIL: GENUINE: a full Hungarian guide with its English overlay; FACT: guide strings that are not in the freeze; FACT: a tracker named in a Hungarian answer (folded, any case)
R2 overlay accepts guide -> 3 case(s) FAIL: GENUINE: a full Hungarian guide with its English overlay; DECOY: a tracker named only in a YAML comment; DECOY: a source-like word only in docs_url (configuration)
R3 checked_version refused in overlay -> 1 case(s) FAIL: FACT: checked_version in the English block
R4 empty steps refused -> 1 case(s) FAIL: FACT: a section with an empty steps list
R5 faq answer required -> 1 case(s) FAIL: FACT: an FAQ entry with a question and no answer
R6 quoted version -> 1 case(s) FAIL: FACT: an unquoted checked_version (YAML reads 4.10 as 4.1)
R7 section/faq counts -> 1 case(s) FAIL: FACT: more English sections than Hungarian
R8 step counts -> 1 case(s) FAIL: FACT: an English section with a different step count
R9 inert English guide -> 1 case(s) FAIL: FACT: an English guide with no Hungarian guide
R10 source deny-list -> 2 case(s) FAIL: FACT: a tracker named in an English step; FACT: a tracker named in a Hungarian answer (folded, any case)
R11 WARN on a moved image -> 2 case(s) FAIL: FACT: the app's image moved off the checked tag (4.0.20 -> 4.0.21); DECOY: the checked tag survives only on a SIDECAR
R12 the app service, not a sidecar -> 1 case(s) FAIL: DECOY: the checked tag survives only on a SIDECAR
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""apply_guide.py — write one draft guide into templates/<app>/.felhom.yml, by TEXT, and prove the parse.
python3 apply_guide.py <catalog-root> <draft.json> [--hidden]
The draft is {"app", "checked_version", "hu": {"sections", "faq"}, "en": {"sections", "faq"}}. The Hungarian guide
goes at the END of the top-level `app_info:` block (two spaces), the English one at the end of `i18n.en.app_info`
(six spaces). An existing guide block is replaced, so a morning format change is one more run. Every string is
written JSON-quoted (valid YAML double-quoted: Hungarian quotes, apostrophes and colons cannot break it — the
„Hungarian-quote YAML kill" trap).
`--hidden` writes `checked_version: ""` (a label was not found: the box does not show the guide).
PROOF, after writing: the file parses; `app_info.guide` and `i18n.en.app_info.guide` equal the draft; and the
WHOLE document minus those two keys equals the document before. Any difference restores the old file and exits 1.
"""
import copy
import io
import json
import sys
import yaml
def q(s):
return json.dumps(s, ensure_ascii=False)
def block(g, ind, cv=None):
p = " " * ind
out = [p + "guide:"]
if cv is not None:
out.append(p + " checked_version: " + q(cv))
out.append(p + " sections:")
for se in g["sections"]:
out.append(p + " - title: " + q(se["title"]))
if se.get("intro"):
out.append(p + " intro: " + q(se["intro"]))
out.append(p + " steps:")
for st in se["steps"]:
out.append(p + " - " + q(st))
if g.get("faq"):
out.append(p + " faq:")
for qa in g["faq"]:
out.append(p + " - q: " + q(qa["q"]))
out.append(p + " a: " + q(qa["a"]))
return out
def indent(line):
return len(line) - len(line.lstrip(" "))
def is_content(line):
s = line.strip()
return s != "" and not s.startswith("#")
def span(lines, start, ind):
"""Index just past the last CONTENT line belonging to the block whose header is lines[start] (children are
indented deeper than `ind`). Trailing blank and comment lines stay outside, with whatever follows."""
end = start + 1
last = start
i = start + 1
while i < len(lines):
if is_content(lines[i]):
if indent(lines[i]) <= ind:
break
last = i
i += 1
return last + 1
def remove_guide(lines, parent, ind):
"""Remove a `guide:` child (at indent `ind`) of the block headed at lines[parent]."""
end = span(lines, parent, ind - 2)
for i in range(parent + 1, end):
if lines[i] == " " * ind + "guide:":
j = span(lines, i, ind)
del lines[i:j]
return True
return False
def find_line(lines, text, after=0, before=None):
for i in range(after, before if before is not None else len(lines)):
if lines[i] == text:
return i
return -1
def strip_guides(doc):
d = copy.deepcopy(doc)
(d.get("app_info") or {}).pop("guide", None)
en = ((d.get("i18n") or {}).get("en") or {})
(en.get("app_info") or {}).pop("guide", None)
return d
def main(argv):
root, draft_path = argv[1], argv[2]
hidden = "--hidden" in argv
d = json.load(io.open(draft_path, encoding="utf-8"))
app = d["app"]
path = "%s/templates/%s/.felhom.yml" % (root, app)
old_text = io.open(path, encoding="utf-8").read()
old_doc = yaml.safe_load(old_text)
lines = old_text.split("\n")
# Hungarian: top-level app_info
ai = find_line(lines, "app_info:")
if ai < 0:
print("FAIL %s: no top-level app_info:" % app)
return 1
remove_guide(lines, ai, 2)
end = span(lines, ai, 0)
cv = "" if hidden else d["checked_version"]
lines[end:end] = block(d["hu"], 2, cv)
# English: i18n -> en -> app_info
i18 = find_line(lines, "i18n:")
en = find_line(lines, " en:", i18 + 1) if i18 >= 0 else -1
if en < 0:
print("FAIL %s: no i18n.en block — every app is translated; refusing to invent one" % app)
return 1
en_end = span(lines, en, 2)
eai = find_line(lines, " app_info:", en + 1, en_end)
if eai < 0:
lines[en_end:en_end] = [" app_info:"]
eai = en_end
remove_guide(lines, eai, 6)
e_end = span(lines, eai, 4)
lines[e_end:e_end] = block(d["en"], 6)
new_text = "\n".join(lines)
new_doc = yaml.safe_load(new_text)
want_hu = {"checked_version": cv, "sections": d["hu"]["sections"], "faq": d["hu"]["faq"]}
want_en = {"sections": d["en"]["sections"], "faq": d["en"]["faq"]}
def norm(g): # the writer omits an empty intro; compare like for like
g = copy.deepcopy(g)
for se in g.get("sections") or []:
if not se.get("intro"):
se.pop("intro", None)
return g
problems = []
if norm(new_doc["app_info"].get("guide")) != norm(want_hu):
problems.append("Hungarian guide does not read back as the draft")
if norm(new_doc["i18n"]["en"]["app_info"].get("guide")) != norm(want_en):
problems.append("English guide does not read back as the draft")
if strip_guides(new_doc) != strip_guides(old_doc):
problems.append("the rest of the document CHANGED")
if problems:
print("FAIL %s: %s — file left as it was" % (app, "; ".join(problems)))
return 1
with io.open(path, "w", encoding="utf-8") as fh:
fh.write(new_text)
print("OK %s: guide written (%d sections, %d faq; checked_version %r)"
% (app, len(d["hu"]["sections"]), len(d["hu"]["faq"]), cv))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""labelcheck.py — every quoted label in a guide draft, looked up in the app's source AT THE PINNED TAG.
python3 labelcheck.py <draft.json> <out.txt> <corpus-dir-or-file> [<corpus> ...]
A label is the text between „ and ” (or “ and ”) in any guide string, Hungarian or English. It is FOUND
when it occurs DELIMITED in a corpus file: as a whole JSON key or value, inside QBT_TR(…)QBT_TR, between
quotes, or between > and <. A bare substring is not enough ("Add" is inside every file). The first hit's
file:line is recorded. Exit 0 = every label found, 1 = at least one MISSING (the guide stays hidden).
The corpus is the app's upstream source at the image's tag (or the files the image itself ships), plus the
controller's own locale files for the Felhom labels a guide points at (a tab name, „Kész, beállítottam").
"""
import io
import json
import os
import re
import sys
LABEL = re.compile(r"[„“]([^”„“]+)”")
TEXT_EXT = (".json", ".js", ".jsx", ".ts", ".tsx", ".html", ".htm", ".vue", ".svelte", ".py", ".go", ".php",
".twig", ".blade.php", ".po", ".pot", ".yml", ".yaml", ".xml", ".ts.xml", ".properties", ".dart",
".kt", ".java", ".cs", ".rb", ".erb", ".hbs", ".mjs", ".cjs", ".svg", ".md", ".txt", ".ini", ".toml",
".strings", ".resx", ".arb", ".csv", ".tmpl", ".gohtml", ".j2", ".jinja", ".lua", ".rs", ".ex", ".exs",
".heex", ".eex", ".elm", ".swift", ".c", ".cpp", ".h", ".ui")
def labels_of(draft):
out = []
for lang in ("hu", "en"):
g = draft.get(lang) or {}
strings = []
for se in g.get("sections") or []:
strings += [se.get("title") or "", se.get("intro") or ""] + list(se.get("steps") or [])
for qa in g.get("faq") or []:
strings += [qa.get("q") or "", qa.get("a") or ""]
for s in strings:
for m in LABEL.finditer(s):
lab = m.group(1).strip()
if (lang, lab) not in out:
out.append((lang, lab))
return out
def files_of(corpus):
if os.path.isfile(corpus):
yield corpus
return
for dp, dn, fn in os.walk(corpus):
dn[:] = [d for d in dn if d not in (".git", "node_modules", "test", "tests", "__tests__")]
for f in fn:
if f.endswith(TEXT_EXT):
yield os.path.join(dp, f)
def find(label, corpora, cache):
e = re.escape(label)
pats = [re.compile(p) for p in (
r'QBT_TR\(%s\)QBT_TR' % e,
r'"%s"' % e, r"'%s'" % e, r'`%s`' % e,
r'>\s*%s\s*<' % e,
r'msgstr "%s"' % e, r'<translation>%s</translation>' % e, r'<source>%s</source>' % e,
r'(?m)^\s*[\w.\-]+\s*=\s*%s\s*$' % e, # .properties / .ini value
r'(?m)^\s*"?[\w.\-]+"?\s*:\s*%s\s*$' % e, # YAML value, unquoted
)]
for c in corpora:
for path in files_of(c):
if path not in cache:
try:
cache[path] = io.open(path, encoding="utf-8", errors="replace").read()
except OSError:
cache[path] = ""
text = cache[path]
if label not in text:
continue
for p in pats:
m = p.search(text)
if m:
line = text.count("\n", 0, m.start()) + 1
return "%s:%d" % (path, line)
return None
def main(argv):
draft = json.load(io.open(argv[1], encoding="utf-8"))
out_path, corpora = argv[2], argv[3:]
cache, lines, missing = {}, [], 0
labs = labels_of(draft)
lines.append("app: %s checked against: %s" % (draft.get("app"), draft.get("checked_version")))
lines.append("corpora: %s" % " ".join(corpora))
lines.append("labels: %d (Hungarian and English guide strings, every „…” occurrence)" % len(labs))
for lang, lab in labs:
hit = find(lab, corpora, cache)
if hit:
lines.append("OK [%s] „%s” %s" % (lang, lab, hit))
else:
missing += 1
lines.append("MISSING [%s] „%s”" % (lang, lab))
lines.append("RESULT: %s — %d label(s), %d missing" % ("ALL FOUND" if not missing else "NOT ALL FOUND",
len(labs), missing))
with io.open(out_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + "\n")
print(lines[-1])
return 1 if missing else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""validate_draft.py — would this draft pass the catalog's copy gate? Checked on a THROWAWAY copy, never the repo.
python3 validate_draft.py <catalog-root> <draft.json>
Copies `templates/<app>/`, `scripts/` and the freeze into a temp directory, writes the guide there with
apply_guide.py, admits the app to the temp freeze, and runs check-copy-i18n.py --root <temp> <app>. Also checks the
night's format: 3-5 sections, each with a title, a one-sentence intro and 2-5 steps; 4-6 FAQ entries; no URL, no
markdown, no bare DOMAIN outside `<label>.DOMAIN`. Prints PASS or the findings; exit 0 / 1. The repo is only read.
"""
import io
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
def fmt_problems(d):
p = []
for lang in ("hu", "en"):
g = d.get(lang) or {}
secs, faq = g.get("sections") or [], g.get("faq") or []
if not 3 <= len(secs) <= 5:
p.append("%s: %d sections (format: 3-5)" % (lang, len(secs)))
if not 4 <= len(faq) <= 6:
p.append("%s: %d FAQ entries (format: 4-6)" % (lang, len(faq)))
for i, se in enumerate(secs):
if not se.get("intro"):
p.append("%s: sections[%d] has no intro (format: one sentence each)" % (lang, i))
if not 2 <= len(se.get("steps") or []) <= 5:
p.append("%s: sections[%d] has %d steps (format: 2-5)" % (lang, i, len(se.get("steps") or [])))
strings = []
for se in secs:
strings += [se.get("title", ""), se.get("intro", "")] + list(se.get("steps") or [])
for qa in faq:
strings += [qa.get("q", ""), qa.get("a", "")]
for s in strings:
if re.search(r"https?://|www\.", s):
p.append("%s: a URL in guide text (not allowed): %r" % (lang, s))
if re.search(r"\*\*|__|`|<[a-zA-Z/][^>]*>|^#|\[[^\]]+\]\(", s):
p.append("%s: markdown or HTML in guide text (rendered as plain text): %r" % (lang, s))
for m in re.finditer(r"\bDOMAIN\b", s):
if not re.search(r"[a-z0-9-]+\.DOMAIN\b", s[max(0, m.start() - 40):m.end()]):
p.append("%s: bare DOMAIN (the box fills it with the household's domain): %r" % (lang, s))
if "—" in s and lang == "hu":
p.append("hu: em dash (R-932): %r" % s)
if re.search(r"[\U0001F300-\U0001FAFF☀-➿]", s):
p.append("%s: emoji: %r" % (lang, s))
return p
def main(argv):
root, draft = os.path.abspath(argv[1]), os.path.abspath(argv[2])
d = json.load(io.open(draft, encoding="utf-8"))
app = d["app"]
probs = fmt_problems(d)
tmp = tempfile.mkdtemp(prefix="guide-validate-")
try:
shutil.copytree(os.path.join(root, "scripts"), os.path.join(tmp, "scripts"),
ignore=shutil.ignore_patterns("__pycache__"))
os.makedirs(os.path.join(tmp, "templates"))
for a in os.listdir(os.path.join(root, "templates")):
src = os.path.join(root, "templates", a)
if os.path.isdir(src):
shutil.copytree(src, os.path.join(tmp, "templates", a))
r = subprocess.run([sys.executable, os.path.join(HERE, "apply_guide.py"), tmp, draft], capture_output=True,
text=True)
if r.returncode != 0:
probs.append("apply: " + (r.stdout + r.stderr).strip())
else:
subprocess.run([sys.executable, os.path.join(tmp, "scripts", "check-copy-i18n.py"), "--root", tmp,
"--add-app", app, "--reason", "validate_draft"], capture_output=True, text=True)
# The ratchet is about the whole catalog; measure it on the temp copy and hand it back.
probe = subprocess.run([sys.executable, os.path.join(tmp, "scripts", "check-copy-i18n.py"), "--root", tmp,
"--expect-missing", "-1"], capture_output=True, text=True)
m = re.search(r"(\d+) strings have no English", probe.stdout)
miss = m.group(1) if m else "0"
r = subprocess.run([sys.executable, os.path.join(tmp, "scripts", "check-copy-i18n.py"), "--root", tmp,
"--expect-missing", miss, app], capture_output=True, text=True)
if r.returncode != 0:
probs += ["copy gate: " + l.strip() for l in r.stdout.splitlines() if l.strip().startswith("- ")
and app in l]
if not any(app in l for l in r.stdout.splitlines() if l.strip().startswith("- ")):
probs.append("copy gate failed:\n" + r.stdout[-1500:])
if miss != "0":
probs.append("English coverage: %s copy string(s) of the catalog would have no English" % miss)
finally:
shutil.rmtree(tmp, ignore_errors=True)
if probs:
print("FAIL %s" % app)
for x in probs:
print(" - " + x)
return 1
print("PASS %s — format and copy gate clean" % app)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+4 -2
View File
@@ -124,10 +124,11 @@ stopping line that lies.
| **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC |
| **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator |
## Apps & catalog — 16 rows (P2 1, P3 9, P4 6)
## Apps & catalog — 17 rows (P2 2, P3 9, P4 6)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
| **R-1086** | Apps & catalog | P2 | **qBittorrent's first steps tell the household to set their own password — and the box's media wiring logs in with the INSTALL password, so after that change Sonarr and Radarr stop getting their downloads handed over and a safe drive removal cannot pause its torrents.** FOUND 2026-10-15 night writing the qBittorrent guide; static, not yet reproduced. `templates/qbittorrent/.felhom.yml` first_steps[1] (set your own password on the Web UI settings page); felhom-controller `internal/mediawire/mediawire.go:124` (the wiring), `:437` (the Sonarr/Radarr client entry), `:757` and `:828` (pause/resume on safe removal) all read `QBT_ADMIN_PASSWORD` from the container env, and the template seeds no login bypass for the Docker network. Transmission has the same shape (`trPW`). Second fault in the same sentence: the qBittorrent Web UI starts in ENGLISH (the image sets no language — its env holds only PATH, read 2026-10-15), so the labels are „Options” and „WebUI”, not the Hungarian words the step quotes. The guide written tonight says nothing about the password. | OPEN — filed 2026-10-15 | Operator: (a) the copy stops advising a password change — the generated one is long, random and shown on the dashboard; or (b) the box asks for the changed password (an after-start field) | Reproduce on 9202 (change it, press „Bekötés most”, read Sonarr's client test), then (a) or (b) | CC |
| **R-981** | Apps & catalog | P3 | **The homepage app gets the Docker socket (that is root in the guest) and has no login, no setup gate and no family gate.** Fix: remove the socket mount, or put homepage behind the family gate. Security review 2026-10-12 (`audits/security-review-2026-10-12/`); detail held off-git for the operator (`/mnt/5_hdd/felhom.eu/evidence/security-review-2026-10-12-PRIVATE/`). | OPEN | — | Change the catalog template; prove on 9202 | CC |
| **R-971** | Apps & catalog | P2 | **A change pushed to the catalog's `main` reaches the installed apps on every box within about 15 minutes, and nothing on the box refuses dangerous container settings (privileged mode, the Docker socket, host mounts, host namespaces); so whoever can push to the catalog repository can take every guest.** Design-shaped: installed apps take catalog fixes on purpose. Decision for the operator in `STATUS.md`. Fix options: a privilege allow-list check on the box before any compose is written (and in catalog CI), and later signed catalog releases. Security review 2026-10-12 (`audits/security-review-2026-10-12/`); detail held off-git for the operator (`/mnt/5_hdd/felhom.eu/evidence/security-review-2026-10-12-PRIVATE/`). | WAITING-ON-OPERATOR — confirmed by a second reader | operator decision | Decide; then build the box-side check | operator |
| **R-676** | Apps & catalog | P3 | **[P3-LOW] Watch: immich's first start restarted 12 times — decision 28's crash-loop stop (6 in 10 min) would stop it.** From the 2026-09-17 chaos night (DB connection dropped during the first-start geocoding import on a 6 GB guest; it did not recover that night). No healthy app in any drill evidence restarts on a first start (1831 samples, 40 live containers), so the threshold stands; this row exists so the first immich install under v0.269.x is watched. `audits/night-2026-09-24/A3/40-first-start-restarts.txt` **2026-09-25 night (read from source, v0.271.0): a DEPLOY's first start is NOT covered by decision 28's suppression** — `Deploying` clears when `compose up -d` returns (`deploy.go` "Clear deploying flag"), and `ObserveUnhealthy` then samples the app; an automatic update's step, verify and undo ARE covered (`Updating`, pinned by `TestD28_NoCrashLoopStopDuringAnAutomaticStep`). So a first start that restarts ≥ 6 times in 10 min is stopped — which R-676 already accepts for a broken first start; a healthy slow first start would be stopped too. **-- 2026-09-30: the first-start restarts are explained.** immich's first-start geodata import OOM-kills its database at 512M on a guest with no swap (R-732, measured: 61–104 kills); the 2026-09-17 chaos-night case (DB connection dropped during the import on a 6 GB guest) fits it. Fixed in the catalog (`56c4888`, 768M). The watch itself (decision 28 on a DEPLOY's first start) is unchanged. | **OPEN — P3; owner: CC (watch)** | — | — | CC |
@@ -307,11 +308,12 @@ stopping line that lies.
| **R-990** | Business & legal | P4 | **Operator: read `documentation/audits/forum-research-2026-10-12/FELHOM.md` §5 and rule on each recommendation.** READ-ONLY research 2026-10-12: three years (#7943–#11092, 2,908 posts, 195 people, 82 beginner threads) of the public PROHARDVER! topic „Home server / házi szerver építése”, tagged and counted by person (`FINDINGS.md`, method and spot-check in `METHOD.md`; no username or post text in git). Ten ranked recommendations, none acted on; the top three: a „which machine works” page (8 of 33 beginners who name their hardware own only a Synology, a router disk or a Raspberry Pi — the installer cannot run there; the same gap is R-989's first recommendation from a different source, and the shipped-page half is R-991), „you do not choose an operating system — we do” (the biggest beginner question, 20 of 82 threads), and „bring your old office PC” as the cheap entry. Catalog gap (≥ 3 people): a DLNA server (7), an ad/DNS blocker (5), a camera recorder (5). | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages | operator |
| **R-1035** | Business & legal | P4 | **Did the „felhő tárhely” page earn anything? Two dated checks.** **Baseline (reported by the operator from Search Console, 2026-10-14, last 3 months):** 1 click, 48 impressions, CTR 2.1 %, average position **73.6**; „felhő tárhely” 19 impressions, „felhőtárhely” 5, „felhő alapú tárhely” 3. CC measured separately that the phrase „felhő tárhely” appeared **0 times** on the home, FAQ and backup pages — Google was showing the home page for those queries by accident. `/sajat-felho-tarhely` + `/en/own-cloud-storage` published **2026-10-14**; full baseline, price sources and the power provenance in `audits/seo-2026-10-14/BASELINE.md`. **What to measure, both times:** impressions and average position for „felhő tárhely” and „saját felhő tárhely”, plus the new page's own impressions, clicks and position. **A null result at +4 weeks means nothing on its own** — a new page on a site at position 73.6 takes weeks to months to move, and „felhő tárhely” alone is held by Microsoft, Google and the big list sites; the page aims at the two longer phrases. That is why there are two checks and not one, and why neither should be read as a verdict on the copy. **There is no Search Console connector in this project, so CC cannot read these numbers** — the operator reads them, CC records and compares. **Blocked until the operator asks for indexing** (Search Console → URL-ellenőrzés → the URL → „Indexelés kérése”): until that happens, or Google finds the page itself, both checks read zero for a reason that is not the page. **This is a slow channel and does not replace talking to people.** | **WATCHING** | operator: request indexing, then supply the Search Console read | 2026-11-11: first read (+4 weeks); 2026-12-09: second (+8 weeks), compared with the first | operator + CC |
## Process & tooling — 29 rows (P3 11, P4 18)
## Process & tooling — 30 rows (P3 12, P4 18)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
| **R-1083** | Process & tooling | P3 | **The scratch box 9202 cannot use any feature that needs the agent: its `bootstrap.json` has no `local_api` block, so its controller never gets an agent client.** MEASURED 2026-10-15 (storage-identity build A): 9202 put on controller 0.317.0 by hand to show that a NAS share no longer hides the one-copy notice (R-1075); its `bootstrap.json` (read on the box) holds no `local_api`, so the controller has no agent to ask and keeps the old `st_dev` rule — the check could not tell old from new — 9202 was put back on 0.314.0. Every agent-coupled feature (drives, same-disk, format, network storage) is untestable on the scratch box today, and the NAS leg of build A is proven by a red-proofed unit test only. Evidence: `audits/storage-identity-buildA-2026-10-15/9202-upgrade.txt`. | OPEN | — | Give 9202 a local-API token the normal way (the agent mints it for its guest), or record in `runbooks/target-selection.md` that 9202 is controller-only; then show the NAS notice live. | CC |
| **R-1085** | Process & tooling | P3 | **Registry tag `felhom-controller:0.316.3` holds another session's uncommitted code; it must never be offered to a box — and the floor can still offer it.** Today the update button offers the highest tag (0.317.0), so nothing reaches it — but any path that picks a lower or specific tag could. The paths that choose a controller version (`architecture/09-update-architecture.md` §0 and decision 250 item 1; felhom-controller `internal/selfupdate/updater.go`), read 2026-10-15 night: (1) the household's button and the daily auto-update — `TriggerUpdate` takes the HIGHEST semver tag in the registry: **cannot** select 0.316.3 while a higher tag exists; (2) the floor, global or per customer — `MaybeAutoUpdate` targets the floor EXACTLY, and the hub's floor save accepts any semver (`hub/internal/web/configs.go` `normalizeFloorInput`): **can** select 0.316.3, and 0.315.0, if anyone types it; (3) the golden — its version is the controller it bakes, and the artifact save refuses a golden older than the newest controller CHANGELOG version (`runbooks/RUNBOOK-manual-build.md` §4): cannot, today; (4) a hand deploy that names a tag (the guest bootstrap deploy in the `felhom-build-deploy` skill): **can**. The fact itself is on file in R-1081 since `bf99798c` (21:48, three minutes after the reviewer checked); this row is the route to the tag, which R-1081 does not cover. Static reading only. | OPEN — filed 2026-10-15 night (the reviewer's item, app-guides session); no code changed tonight | — | A list of spoiled tags (0.315.0, 0.316.3) refused by both floor forms and named in the deploy runbook, with a decoy that types 0.316.3 into the floor | CC |
| **R-1081** | Process & tooling | P4 | **Two published controller tags hold content that is not what their number says: `0.315.0` and `0.316.3`.** Both were caused by CC on 2026-10-15, and both by the same shape — a guard that was not on the path the work takes. **`0.316.3`:** the build directory's `build.sh` is a symlink into the SHARED clone's working copy, which predated the `FELHOM_REPO_DIR` override, so the override was silently ignored and the build copied that tree — which held 31 uncommitted files of another session's physical-disk storage work. The banner printed `Commit: 0225255` with no `Repo:` line. **Nobody deployed it; that was luck, not a control.** Fixed at the root in controller v0.316.4: `build.sh` now refuses `--push` from a tree that is dirty, not at `origin/main`, or not a git checkout, with four test cases red-proved. **Neither tag is re-pushed or deleted** — a version number is pushed once, and that applies to numbers CC spoiled. **No box runs either:** demo-hp 9201 is on 0.317.0, the floor is 0.314.0, and the button offers the highest tag. **`0.315.0`:** Caused by CC on 2026-10-15 while proving R-1080's own fix: the guard was added to `controller/build.sh` in the repo, but the documented release command runs `$FELHOM_ROOT/build/felhom-controller/build.sh`, which was a **separate copy taken 2026-07-18** and had no guard. Running the refusal against a real existing tag therefore rebuilt and re-pushed 0.315.0 — a fourth time — with v0.316.0's code under it (digest `8887343011d6` → `b809201841f7`). **Fixed at the root in the same session:** the build directory's script is now a symlink to the repo's, the old copy kept beside it as `build.sh.stale-copy-2026-07-18`, and the refusal was then proven to fire from the documented path. **No box is affected:** nothing runs 0.315.0 (demo-hp 9201 is on 0.316.2), the floor is 0.314.0, and the household button offers the highest tag. What remains is two tags that must never be deployed and are no longer reproducible from the registry. | **OPEN** — filed 2026-10-15; nothing to do unless someone needs one of those two images, and then the answer is the current release | — | Leave them. If an audit ever needs the real 0.315.0 or 0.316.3, build it from the matching commit under a NEW number and say so | CC |
| **R-992** | Process & tooling | P3 | **The drill-catalog route that `09` §6.5 documents cannot be followed: `admin/app-catalog-drill` is PRIVATE and no box has catalog credentials.** Measured 2026-10-12 while publishing the Usenet pair: the box's clone fails with `fatal: could not read Username for 'https://gitea.dooplex.hu'` (exit 128), the Gitea API reports the drill repo `private=true` and the live catalog `private=false`, and NEITHER the current controller.yaml on 9202 NOR any of its 30 saved copies has ever carried `git.username` or `git.token` (the controller supports both — `internal/config/config.go` GitConfig). The route worked for the campaigns of 2026-10-10 and the torrent pair earlier the same day, so the repository's visibility changed between then and now. **Worked around, not fixed:** that campaign staged its two templates in the box's own catalog cache (`audits/usenet-2026-10-12/tools/stage_in_cache.py`), which is where the sync copies a catalog change anyway, with the sync interval raised to 12h for the duration and rescan used in place of sync. The real fix is an operator decision: either a read-only catalog credential on the scratch box (a secret on a Tier-0 guest) or making the drill repository readable. Until then every new-app campaign hits this. | WAITING-ON-OPERATOR | the operator's choice between a box credential and a readable drill repo | Decide; then either set `git.username`/`git.token` on 9202 from the credentials store, or flip the drill repo, and correct `09` §6.5 either way | operator |
| **R-1017** | Process & tooling | P3 | **A comment names a guard test that does not exist.** `felhom-controller/controller/internal/appbackup/paths.go:58` says „`TestNoBareDrivePathToUserdataDir` is the guard that keeps the count from growing”; no test of that name (or `NoBareDrive*`) exists in the repo (grep 2026-10-14; positive control: `TestRemoveStack_R800_UserdataKeptIsNamed` is found). A comment asserting an invariant with no test pinning it — the class in the `felhom-testing` skill. | **READY** — filed 2026-10-14 | — | Write the guard (a test that fails when a bare drive path reaches `UserdataDir`), red-proof it, or correct the comment | CC |