Files
serverAppConfigs/serverAppConfigs-docs/FrankBook-Pro/ARR-Stack.md
Frank 327e78b950 docs(vault): destroy the dead weight — 53 pages and the change diary
The vault was 155 pages / 31,210 lines, most of it measurement ledgers for
payloads that no longer run. A throughput figure for a runtime nobody operates is
not a record: AI moves faster than the vault, so the number is wrong before
anyone reads it, and a wrong number in a runbook gets believed. Per the new
purpose test in `AGENTS.md` → *Documentation Policy*, it is destroyed.

Gone (`.AGENTTRASH/20261019-destruction/`, KILL-LIST.txt is the manifest):
- the `frank-medium` endpoint and its five era pages, `frank-npu`, FastFlowLM,
  the ikllama/kingjones/dealign/frank-redo eras, the ling/swift15/freetoken/ubatch
  campaign pages, qwen3.8 spec-matrix and strix-optimizations, the two
  "Running Qwen3.6 …" pages;
- the whole `Pipeline Parallelism/` folder (no live script passes a pipeline flag)
  and the multi-device/x4-pair/hybrid/offload experiment pages;
- every vLLM page — no vLLM endpoint exists on this fleet;
- `LLM/deployment.md` (`Architecture/Deployment Flow.md` is the test-referenced
  owner) and the external surveys (SGLang, Lemonade, llama.cpp forks/diff,
  self-hosted app census, the TTS survey) that were obsolete by construction;
- `log.md`, 2,380 lines. Git holds the chronology; a causal law belongs to the
  page that owns the thing it governs, never to a diary.

Survivors: 101 pages, 17,542 lines, 0 OKF violations, 0 unresolved links. Eras
tables keep the window, payload, runtime, reason and rollback pointer; the rate
columns are blanked. The live shapes stay verbatim — those numbers are what the
deployer and `models.yml` consume.
2026-10-19 10:56:37 -07:00

6.1 KiB

type, title, description, tags, timestamp
type title description tags timestamp
reference *arr Stack The *arr automation stack on FrankBook-Pro, every container on the VPN netns: topology, wiring, and gotchas.
arr-stack
prowlarr
sonarr
radarr
lidarr
readarr
qbittorrent
frankbook-pro
dankserver
2026-09-04T00:00:00Z

*arr Stack

The *arr automation stack runs on FrankBook-Pro (the Mac mini) as quadlets inside its podman machine; all egress rides the gluetun WireGuard netns. The per-service pages (Prowlarr, Sonarr, Radarr, Lidarr, Readarr, qBittorrent) carry deployment facts; this page holds the cross-cutting API and auth-posture procedures.

API version split (memorize before any *arr HTTP call)

  • Sonarr/Radarr: /api/v3/…; Lidarr/Readarr/Prowlarr: /api/v1/….
  • Prowlarr is port 9696 — 8090 is Jackett.
  • Empty body = endpoint absent, not a failure: probe endpoints by status code first; v1 has no /api/v1/profile endpoint at all.
  • PUT /downloadclient and PUT /indexer/{id} take a SINGLE resource object (GET it, patch settings, PUT the whole object back). An array body fails with Path: $ | BytePositionInLine: 1.
  • API keys live in each container's config: podman exec <svc> grep -o "<ApiKey>[^<]*" /config/config.xml | cut -d">" -f2. They are local-only secrets — they stop being secret the moment a vhost serves the API unauthenticated.

Schema-first probing (validators are free documentation)

Send the FULL schema template object with fields filled; v1 validators reject partials, and each error names the next required field — peel validators one live call at a time instead of guessing payload shapes:

  • 'Fields' must not be empty → send a full schema copy, not fields:[].
  • 'Base Url' must not be empty when keys were camelCase → PascalCase is required (BaseUrl, ApiKey) — schema keys, not the JSON wire keys.
  • DefaultMetadataProfileId must be > 0 → fetch real ids from GET /api/v1/metadataprofile (rows like (1,'Standard')) GET /api/v1/qualitydefinition returns the quality CATALOG — ids ≠ profiles.
  • targetType null → configContract not registered in that build (this Prowlarr build has no Prowlarr-plugin indexer; integration is Torznab-family only).
  • testingPackageId in a PUT body downgrades validation failures to warnings.
  • syncLevel is enum ApplicationSyncLevel — omit it or use the int; a string is rejected.

Verification rules: POST /downloadclient returns the COLLECTION, not the created object — verify via GET. Test endpoints return verdicts in the BODY at HTTP 200 ({} = pass, a list = fail) — judge from bodies, not status codes. Command POSTs (MoviesSearch, ApplicationSync) run server-side and respond only when the work finishes: curl -m timeouts (HTTP 000) during a search are NOT failures — poll queue/history afterward instead of re-firing; warm small commands (RefreshMonitoredDownloads) answer in ~20 ms, so a hang means live work in progress, not a dead API.

DB as arbiter: when endpoint shapes are opaque, stop the container and edit /opt/prowlarr/config/prowlarr.db with sudo python3 sqlite directly, then start. The indexers table columns are Id/Name/Implementation/Settings/ConfigContract/Enable/Priority/Added — the enable flag column is Enable (not EnableRss). Prowlarr's real application settings values live in prowlarr.db → Applications.Settings JSON (the /api/v1/applications GET responses carry fields: []).

Auth posture probe (ground truth is behavior, not config flags)

AuthenticationMethod=None + AuthenticationRequired=Enabled in config.xml is ambiguous on paper; on the wire it means a login screen with zero users — first-account-wins.

  1. Confirm which host runs the service before probing (podman ps on the target, not the box you ssh'd into first — the arrs live on FrankBook-Pro).
  2. Probe an API endpoint that bypasses the SPA shell, locally and through any vhost: curl -s -o /dev/null -w "%{http_code}" http://<host>:<port>/api/v3/system/status. SPA roots (/) always serve a 200 shell — never conclude "open" or "gated" from /. 401 = gated; 200 + JSON = API fully open (anyone on the net can drive it with curl).
  3. For config truth, dump the whole file (podman exec <svc> cat /config/config.xml), not a piped grep | sed -n 2p: sed -n Np exits 0 on zero matches, so the pipeline hides a miss and you conclude "empty field" when the grep itself failed.
  4. Decision framing: when posture = gated-with-zero-users, present it as a decision menu (None behind the front-door wall vs External/authentikarr) not a silent change — first-account-wins means whoever logs in first owns the instance.

The None + Enabled auth-config trap (v1 .NET apps)

<AuthenticationMethod>None</AuthenticationMethod> + <AuthenticationRequired>Enabled</AuthenticationRequired> is self-contradictory: the UI Save is permanently disabled, and any save throws DryIoc.ScopedItemException in AuthorizationMiddleware → 500 on every page after restart. The valid no-auth representation is absence of the <AuthenticationRequired> key, not the literal string Disabled. Fix = delete the key from /config/config.xml (sed -i "/<AuthenticationRequired>/d"), restart the container. Tell-tale: one v1 app boots clean while siblings 500-sprawl → diff their config.xml auth blocks; the healthy one shows the correct shape. (Prowlarr runs Forms + Enabled healthily — the crash combo is None+Enabled only.)

General rules that saved the session these came from: container State=running ≠ app serving — read podman logs --tail 12 <c> before theorizing; when a UI gate blocks saving, go under it (these apps keep plaintext XML config on disk — read/edit via podman exec).

See Also