Fix System page crash, retire "held" copy, and slim CLAUDE.md
All checks were successful
Deploy / deploy (push) Successful in 27s

- system.html crashed with a 500 whenever SABnzbd history loaded: the
  trend() macro compared the formatted "avg_speed" string (e.g. "900
  KB/s") against a number. Added a real numeric avg_speed_kbps field
  and pointed the macro at that instead.
- Replaced "held"/"not held" wording with "in library"/"not in
  library" on Home and Discover.
- Moved CLAUDE.md's Deployment/reverse-proxy and account-management
  sections into two on-demand skills (deploy-arr-summary,
  manage-accounts) so they only load when actually relevant, with a
  one-line pointer left in their place.
- Updated PRODUCT.md's restructure note now that meridian-marquee has
  merged to main.
- Dropped two stale/broken entries from settings.local.json's
  permission allowlist (a rule from a different machine/path, and one
  with a corrupted token).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBw1QodG8ZMYHrGeQfqyDS
This commit is contained in:
Barely Removable 2026-08-30 08:05:49 -07:00
parent bb3e9af056
commit 08491c0c9b
9 changed files with 153 additions and 133 deletions

115
CLAUDE.md
View file

@ -166,119 +166,14 @@ app from a shell on an explicit `PORT` is the working path.
## Deployment
**Target:** Unraid server, container path `/mnt/user/appdata/greendale`
(note the directory name differs from the container/repo name), built from
`Dockerfile`/`docker-compose.yml` in this repo. Container name
`arr-summary`, port `5055` (overridable via `HOST_PORT`) → container port
`5000`.
**Deploy mechanism: Forgejo push-to-deploy**, same pattern as the other
three projects on this box — see `card-grader`'s `CLAUDE.md` for the full
pipeline mechanics. Push to `main` on the `forgejo` remote
(`https://git.hippofam.com/ninja_hippo/arr-summary.git`) triggers
`.forgejo/workflows/deploy.yml`, which syncs into
`/mnt/user/appdata/greendale` (excluding `.git`, `.env`, `data/`) and runs
`docker compose up -d --build`.
**Restarting the container manually: use Compose, not the Unraid Docker
UI.** Every deploy recreates the container with a new Docker ID (same
name, `arr-summary`). Unraid's Docker tab caches the old ID, so hitting
Restart there after a deploy throws "No such container" — harmless, but
confusing. From the Unraid terminal:
```bash
cd /mnt/user/appdata/greendale && docker compose restart
```
`docker compose up -d --force-recreate` for a guaranteed-clean recreate;
`docker ps --filter name=arr-summary` to check real state regardless of
what the Unraid UI shows.
**Confirming a deploy actually landed:** the dashboard footer renders
`v<APP_VERSION> · <APP_VERSION_DATE>` from constants near the top of
`app.py` (both env-overridable, injected into every template by the
`inject_version` context processor). Bump them when cutting a deploy —
loading the page is then the quickest honest check that the new build is
live, without trusting the Unraid Docker tab's cached container IDs.
## Reverse proxy (Nginx Proxy Manager)
Two NPM proxy hosts front this app and `git.hippofam.com`, both using
Let's Encrypt certs issued via **DNS-01 through Porkbun** — required,
not optional: **Cox blocks inbound port 80** on this connection, so
HTTP-01 challenges (and plain `http://` requests generally) can never
work here. `http://hippofam.com` will always time out from outside the
LAN; only `https://` resolves. Both proxy hosts have Force SSL on.
`hippofam.com` → `http://192.168.86.33:5055` (the Unraid box, this
container's port). Verify the destination IP/port here before assuming
an outage is DNS or app-level — it has drifted to the wrong host before.
On 2026-08-26 the apex host's own cert (issued Aug 2025) went dark: NPM's
UI still showed it "In Use" with a valid, unexpired date, but nginx
presented no certificate at all for `hippofam.com` (TLS alert 112,
"unrecognized name") — the on-disk cert files had rotted independently
of the DB record NPM's UI reads from. `git.hippofam.com`, on a newer
cert from the same account, was unaffected. Fixed by pointing the proxy
host at the `*.hippofam.com` + `hippofam.com` wildcard cert already
issued and sitting unused, which forced NPM to regenerate the nginx
config with known-good cert files. If `hippofam.com` ever throws a TLS
handshake error again with NPM's UI reporting all green, suspect this
same on-disk/DB drift before anything else — check with:
```bash
openssl s_client -connect <public-ip>:443 -servername hippofam.com </dev/null
```
A clean run shows `subject=/CN=...` and `Verify return code: 0`; the
broken state shows `no peer certificate available`.
**Persistent state, never touched by deploys:** `.env` holds every API key
and `LOGIN_PASSWORD` — this project was already doing this correctly
before the Forgejo migration (compose file uses `${VAR}` substitution from
`.env`, no hardcoded secrets), unlike `n64-tracker` which needed fixing.
Runtime settings written by the Settings page (API-key overrides,
timezone, changed login password) live in `settings.json` at
`SETTINGS_FILE`, which the compose file points at `/data/settings.json`
on the `./data` bind mount — i.e. `/mnt/user/appdata/greendale/data`.
That mount is load-bearing: without it the file sits on the container
filesystem, and every `up -d --build` recreates the container and
silently discards it. `data/` is likewise excluded from the deploy
rsync, which runs `--delete` and would otherwise remove it for not
being in the repo.
See the `deploy-arr-summary` skill for the Forgejo push-to-deploy
pipeline, manual container restart steps, deploy verification, and the
Nginx Proxy Manager / TLS setup and its Cox-port-80/DNS-01 constraint.
## Logins
Two modes, chosen by whether `settings.json` holds a non-empty `users` map:
- **Multi-user** (once any account exists): username + password, each
password stored as a salted pbkdf2 hash under `users` in `settings.json`.
`verify_credentials()` is the only entry point; `load_users()` re-reads
the file per check, so adding a user or changing a password is live
immediately in every gunicorn worker — no restart, unlike the API keys.
- **Legacy single shared password** (no accounts yet): the original
`LOGIN_PASSWORD` / `settings.json.login_password` path, username ignored.
This fallback exists so deploying multi-user support can't lock an
existing install out; the login page hides the username field entirely
until accounts exist.
Manage accounts with `manage_users.py`, which prompts for passwords
(never echoed, never a CLI argument, so they stay out of shell history)
and only ever writes hashes:
```bash
docker exec -it arr-summary python3 manage_users.py list
docker exec -it arr-summary python3 manage_users.py add <username>
docker exec -it arr-summary python3 manage_users.py passwd <username>
docker exec -it arr-summary python3 manage_users.py remove <username>
```
**Adding the first account switches the whole dashboard to per-user
logins and the shared password stops working** — verify you can sign in
as that user before closing the shell. `remove` refuses to delete the
last account for the same reason.
Users live in `settings.json` rather than an env var deliberately: a
pbkdf2 hash contains `$`, which docker compose interpolates when reading
`.env`. `PASSWORD_HASH_METHOD` is pinned to pbkdf2 rather than Werkzeug's
default scrypt, which needs a hashlib built against OpenSSL and is absent
on the macOS system Python.
See the `manage-accounts` skill for the multi-user vs. legacy
single-shared-password login modes and the `manage_users.py` CLI.
**Never put a real password in the repo** — not in `app.py`, not in
`settings.json` committed to git, not in a fixture. `.env` and `data/`