The catalog published SABnzbd and NZBGet today (app-catalog 8ee687e). Here: - two cards in the media category on both language sets, each with the project's own logo and each saying IN BOLD that a paid Usenet subscription is the household's own - the published count 60 -> 62 everywhere it is stated; the category badges are derived from the cards now (media read 15 with 17 cards, otthon/home read 14 with 16 — both stale) - documentation/audits/usenet-2026-10-12/: both bench runs, both box steps and verdicts, the nine defects with their measurements, what a missing provider left unmeasured, the teardown - R-988 closed (moved to CLOSED-ITEMS in this commit); R-989 filed: the drill-catalog route of 09 §6.5 cannot be followed — that repo is private and no box has catalog credentials, so the templates were staged in the box's own catalog cache instead - walk.app_env: a SECRET field in app.yaml is stored ENC:, so that helper returns ciphertext — the docstring says so now, measured the hard way - STATUS.md and REPORT-usenet-2026-10-12.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
The Usenet pair for the catalog — SABnzbd and NZBGet (2026-10-12, R-988)
Why. The operator asked for Usenet clients beside the torrent pair published the same day ("sabnzbd, nzbget for example"). Radarr, Sonarr and Jellyseerr can hand a job to a Usenet client exactly as they hand one to a torrent client, and the catalog had none.
Where this got to. Both templates are written, measured in two venues, and published:
app-catalog-felhom.eu — SABnzbd 5.1.3 (lscr.io/linuxserver/sabnzbd) and NZBGet v26.3
(nzbgetcom/nzbget, the project's own image), each with its complete onboarding record and a first
ladder step proven on the bench (LXC 9401) and on scratch guest 9202 through the product.
What is NOT measured, and why it is stated first
A household must bring a paid Usenet provider, and this session had none. So the download chain — connect to a provider, authenticate, fetch articles, par2 repair, unpack, the finished file on the drive — is not measured, and neither is memory under that load. Both app pages say the subscription is required before anyone installs, and both records carry the gap. Everything else is measured, including each app's queue taking a job through its own API and keeping it across the product's Update, which is the store an upgrade must not lose.
The operator was offered three ways to close the gap (a provider's free trial, a free text-only server to prove the outbound path, or publishing with the gap stated) and the third was taken for this round. A trial account would add one bench phase.
What is in here
sabnzbd-bench/, nzbget-bench/ |
the twelve questions asked of each app on LXC 9401 (image, telemetry, entrypoint, first admin, the Host header, storage, ownership, health, the seed with no provider, the config as the app rewrote it, memory, the log), plus 14-both-after-the-fixes.txt and SABnzbd's 15-sabnzbd-login.txt, and MV-*/ — the harness verdict for each ladder step |
box/ |
guest 9202 through the product: boxwalk-results.json (install, stranger, household, lock-out), each app's step.txt + box-verdict-<app>.json (the product's guarded Update), the app logs, the settings each app ended up with |
tools/ |
stage_in_cache.py (why the drill route could not be used, and what replaced it), boxwalk_usenet.py, boxstep_usenet.py, boxstep_sabnzbd_again.py |
Nine defects, each found by measuring and fixed before publication
NZBGet
- The image has no curl, so the healthcheck could never pass. It carries busybox wget, python3
and openssl (measured), so the probe is the BusyBox/wget family from
REUSE.md: rc 0 against the running app, rc 1 against a port nothing serves. - The app's queue, nzb files, temp dir and log were on the household's DRIVE. NZBGet ships
MainDir=/downloads, and this template's own backup block calls that folder "optional, fetchable again" — so a restore would have brought back the finished files and lost what was downloading. The seed movesMainDirto/config/data, inside the volume classifiedimportant; only the finished and half-assembled files stay on the drive. - The shipped config file carries the well-known default password in plain text. The image
overrides it on the command line (
-o ControlPassword=...), so the default does not work — but the file keeps advertising it, and the app's own CLI reads the FILE and so could not talk to its own server ("No response or invalid response", measured). The seed writes the generated password into the file: the default is gone (grep count 0) and the CLI works. - The shipped config asks nzbget.com for new versions (
UpdateCheck=stable) and ships the article cache and write buffer off, which the app itself warns about on every start. The seed setsUpdateCheck=none,ArticleCache=100,WriteBuffer=1024.
SABnzbd
- The app was open to every visitor.
inet_exposure = 5("web UI with login for external") is decided from the CLIENT ADDRESS, and SABnzbd's built-in LAN ranges include the 172.16.0.0/12 that traefik arrives from — so the login applied to nobody. Measured before the fix: a request with no credentials at all answered 200 and served the Quick-Start Wizard.local_ranges = 127.0.0.1replaces those defaults, so nothing a proxy presents is local; the same list feedsxff_trusted_networks, so it also stops the app trusting anX-Forwarded-Forheader, which the tunnel's chain can carry client-written (R-753's family). Read insabnzbd/security.pyandsabnzbd/misc.py, then measured in both directions on the bench. - The healthcheck dialled
/, which answers 303, so the container could never become healthy. The probe is the API's version call now, accepting 200, 401 or 403 — all three mean the web server answered. - The controller REJECTED two blocks of
.felhom.yml:backup.userdata["media/usenet"]anddata_paths[userdata/media/usenet]"matches no compose bind", because the compose bound the two subfolders and the blocks named their parent. Both were inert — the downloads were in no declared backup class at all. One bind now, with the app's owncomplete/andincomplete/inside it. after_installalways failed, so the install HELD and the box kept the app closed ("this app is waiting for its first setup", measured on 9202 — the gate doing exactly its job). The readback greps expected"key": value; SABnzbd's API answers"key":valuewith no space. With-Eand an optional space the marker prints, and the three settings that matter are read back out of the running app.
The fixtures themselves
- Two defects in the test tooling, both of the same shape — assuming a tool exists inside a
container. The bench NZBGet fixture encoded the NZB with
base64 -w0and called curl inside an image that has neither; it talks from the harness host now. The box SABnzbd fixture read the API key fromapp.yaml, where a deploy secret is stored encrypted (SAB_API_KEY: ENC:...), so the app answered "API Key Incorrect"; it reads the value the app itself holds now.walk.app_env's docstring was corrected to say what anENC:value does, since the next session would have made the same mistake.
And one finding that was my measurement, not the app
NZBGet looked broken — Could not create file /downloads/tmp/nzb-1.tmp, and the queue refused the
job. The cause was my own probe: I ran the app's CLI with docker exec, which defaults to root in
this image, and that recreated the app's working directories as root. The app, running as uid 1000,
could then write nothing. Re-measured from a clean deploy with no root-run probe, the queue takes the
job. Second time today that measuring from the wrong place convicted an innocent app (the
afternoon's apparent household lock-out was the same shape), which is why both are written down.
The drill route is broken, and what was used instead
09 §6.5 says a new app's box walk runs from the drill catalog. It cannot today:
admin/app-catalog-drill is private (Gitea API: private=true, while the live catalog is
private=false), and no controller.yaml on 9202 — current or any of its 30 saved copies — has ever
carried git.username/git.token. The box's clone fails with
fatal: could not read Username for 'https://gitea.dooplex.hu', exit 128. The route worked for the
campaigns of 2026-10-10 and the torrent pair this afternoon, so the repository's visibility changed
between then and now.
Rather than put a credential on a box or change a repository's visibility unasked, the two templates
were staged in the box's own catalog cache (tools/stage_in_cache.py) — which is where the sync
copies a catalog change anyway, so the box deploys, scans, badges and updates exactly as it would from
a catalog; the only thing not exercised is the git fetch, which is not what a new app's box walk
measures. The image move for the ladder step was made in the same place, which is what drill_bump
does to the repo. Two guards: the sync interval went to 12h for the duration (restored at teardown)
and the walk calls rescan, never sync. Filed as a register row (R-992) — the fix (a catalog credential on
the box, or a public drill repo) is an operator decision.
What the two venues said
| bench LXC 9401 | box 9202, through the product | |
|---|---|---|
| SABnzbd version-5.1.2 → version-5.1.3 | proven — seed read before and after, healthy, abort starts-and-serves, peak 78 MB = 7.1 % of 1024M, 0 kills, 8344 requests (all 303: the login redirect, i.e. closed) | proven — the guarded Update ran backing-up → pulling → copying → starting → verifying → done in 27.7 s; the queued job AND the category read back |
| NZBGet v26.2 → v26.3 | proven — seed read before and after, healthy, abort starts-and-serves with the job still queued, peak 8.8 MB = 1.7 % of 512M, 0 kills, 8404 requests (all 401: closed) | proven — the guarded Update ran backing-up → pulling → copying → verifying → done in 15.4 s; the queued job read back |
C3, the harness's negative control, came back failed before either edge was run, as the rule
requires.
First admin, both class 1
| how | measured | |
|---|---|---|
| SABnzbd | the entrypoint seeds sabnzbd.ini with the generated login, the generated API key, inet_exposure = 5 and local_ranges = 127.0.0.1; after_install reads three of the four back out of the running app |
a stranger gets /login; /config/ and /wizard/ redirect there; the household's session reaches /config/ with 200; a wrong password gets no session; the API answers the generated key and refuses a wrong one; six wrong sign-ins lock nobody out |
| NZBGet | the image applies NZBGET_USER/NZBGET_PASS on every start, and this template also writes them into the config file so the shipped default is not left there |
the generated password 200, a wrong one 401, the documented default nzbget / tegbzn6789 401, the default name with our password 401, six wrong tries lock nobody out |
State left behind — three layers, verified
box/teardown-usenet.json holds the measurements.
- Machine (guest 9202): both apps removed through the product (stop, then remove with the
drive data),
deployed: falsefor both, both config volumes gone, no container left, and the two download folders deleted from the scratch drive. The guest's standing apps (filebrowser, paperless) are untouched and running. - The staging undone: the two templates are out of the box's catalog cache and out of
/opt/docker/stacks, and the sync interval is back from 12h to 15m. The catalog pointer was never changed away from the live catalog for this campaign (the staging needed it), and the restoration is proven from a different channel than the controller's own verdict (R-286): the sha256 ofsabnzbd,nzbgetandgrocy's.felhom.ymlin the box's cache equals the sha256 of the same files at the publishedorigin/main, with a decoy pair that must differ and does. The two apps are in that cache again legitimately — they are published now. - Host (demo-hp): bench LXC 9401 has no containers and no volumes (the three anonymous volumes
this campaign left were removed by name — never a prune) and is stopped, as it was found. The
drill catalog is reset to the live
main, and the DooPlex catalog clone is clean at the published commit. - Hub: nothing to undo — 9202 reports to no hub by its own configuration.
- One thing that is NOT a leftover:
media/torrentsandmedia/transmissionexist on the scratch drive again, empty (4–8 KB), although the afternoon's teardown deleted them. The product's own storage sync recreates a published app's declared folder so the file browser can show it; these hold nothing and were not created by this campaign's tests.