# REPORT — 2026-10-12 (late night): the Usenet pair published (R-988) The operator asked for SABnzbd and NZBGet and asked one question first: does testing need a Usenet account? Answered with the line the work then followed — no, except for the download chain itself, which is recorded as unmeasured rather than glossed over. ## What shipped - **`app-catalog-felhom.eu` `8ee687e`** — `templates/sabnzbd/` and `templates/nzbget/` with `onboarding/sabnzbd.md` and `onboarding/nzbget.md` in ONE commit, plus the README tables, two `FIRST-ADMIN.md` class-1 rows and a fixture per app in both venues. - **This repo** — the two app cards (both language sets) with the projects' own logos, the published count 60 → 62 everywhere it is stated, corrected category badges (now derived from the cards), `documentation/audits/usenet-2026-10-12/`, the register changes, and this report. ## What was measured, and what was not Bench LXC 9401 (twelve questions per app) and scratch guest 9202 **through the product** (install from a staged FROM pin, stranger and household through traefik on the app's own public name, the guarded Update with its per-app backup, both remove choices). Both ladder steps are `proven` in both venues, written by `upgrade-test.py --write-ladder`; C3, the harness's negative control, came back `failed` first. **Not measured, and stated in both records and on both app pages:** a household must bring a PAID Usenet provider, and this session had none, so connecting to a provider, par2 repair, unpacking, the finished file on the drive, and memory under that load are untested. The queue — the store an upgrade must not lose — is measured in both apps, seeded through their own APIs with a hand-built NZB whose articles do not exist. The operator was offered a trial account or a free text-only server and chose to publish with the gap stated; adding the download measurement later is one bench phase. ## Nine defects, found by measuring Four are lessons beyond these two apps. **`inet_exposure = 5` protected nobody** in SABnzbd: it asks only an "external" visitor to log in, and the app's built-in LAN ranges include the 172.16/12 that traefik arrives from — a request with no credentials at all answered 200 and served the Quick-Start Wizard, until `local_ranges = 127.0.0.1` replaced those defaults (which also stops the app trusting `X-Forwarded-For`). **NZBGet kept its queue, nzb files and temp dir under `MainDir`, which ships as `/downloads`** — the household's drive, the one folder this catalog classifies "optional, fetchable again", so a restore would have returned the finished files and lost what was downloading. **A `backup` or `data_paths` entry must name a path the compose actually binds**, or the controller rejects the block out loud and the data has no class at all (SABnzbd's first version bound two subfolders and declared their parent). **An `after_install` readback must match the app's own JSON spacing** — SABnzbd answers `"key":value`, the greps expected a space, so the marker never printed, the install held and the box kept the app closed, which is the gate working exactly as designed. The rest: NZBGet's image has no curl, so the healthcheck could never pass (busybox wget now, proven both ways); its shipped config carries the documented default password in plain text and thereby breaks the app's own CLI; it asks nzbget.com for new versions; SABnzbd's healthcheck dialled `/`, which answers 303; and two of my own fixtures assumed tools inside containers that lack them. **Two measurement artefacts, recorded as such.** NZBGet looked broken because my probe ran its CLI with `docker exec`, which defaults to root in that image, and recreated the app's directories as root. And the box fixture read the API key from `app.yaml`, where a deploy secret is stored **encrypted**, so the app rightly answered "API Key Incorrect"; `walk.app_env`'s docstring now says what an `ENC:` value does, since the next session would have made the same mistake. ## The documented drill route could not be followed — filed, not papered over `09` §6.5 runs a new app's box walk from the drill catalog. `admin/app-catalog-drill` is **private** (Gitea API: `private=true`; 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`**, so the box's clone fails with `fatal: could not read Username`, exit 128. The route worked for the campaigns of 2026-10-10 and the torrent pair earlier today, so the repository's visibility changed in between. 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 — 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. **R-992** asks the operator to choose the real fix. ## Gates and CI `catalog_gates.py --fast` (whole catalog): all OK. `catalog_gates.py sabnzbd nzbget`: the same but for `image-resolvable` (needs network) and `volume-persistence` (refuses a host that is not a scratch host) — the two documented periodic ones, both covered on the bench. `site_gates.py`: OK, including the logo gate (64 catalog apps, each with its logo) and the sitemap. `repo_gates.py --fast` here still reports `instructions` and `script-tests` failing and `wire-contract` inconclusive — the same pre-existing, Windows-shaped results as this afternoon, none of them about this work; CI on the DooPlex-shaped runner is the authority and this afternoon's run was green. ## Teardown — three layers, verified Both apps removed **through the product** on 9202 (volumes gone, no containers, the two download folders deleted from the scratch drive, the standing apps untouched); the staging undone (out of the cache and out of `/opt/docker/stacks`, sync interval back to 15m), with the catalog state proven by sha256 against the published `origin/main` plus a decoy that must differ; bench 9401 emptied — including the three anonymous volumes it was left with, removed **by name**, never a prune — and stopped as it was found; the drill catalog reset to live `main`; the DooPlex clone clean at the published commit. Hub: nothing to undo. `media/torrents` and `media/transmission` exist again on the scratch drive as empty directories, created by the product's own storage sync for published apps, not by this campaign. ## Register Rows before 117, after 120 - a parallel session filed three of its own (R-989, R-990, R-991) while this one ran, which is also why the drill-catalog row is **R-992** and not R-989. This session opened 1 (**R-992**) and closed 1 (**R-988**, moved to `CLOSED-ITEMS.md` in the same commit). Fixed without a row: the nine defects above, the two fixture defects, the stale category badges on the app page, and `walk.app_env`'s docstring.