arr-summary/CLAUDE.md
Barely Removable 600a48da93
All checks were successful
Deploy / deploy (push) Successful in 21s
Persist settings.json across deploys via a data volume
The previous commit made Settings-saved API keys override .env, but the
key still never survived: settings.json was written to the container
filesystem, and every deploy runs `up -d --build`, which recreates the
container and discards it. Saving a rotated SABnzbd key and restarting
therefore still fell back to the stale .env value.

SETTINGS_FILE is now configurable and pointed at /data/settings.json on
a ./data bind mount, so runtime settings live on the host. The deploy
rsync also excludes data/ — it runs --delete and would otherwise wipe
the directory for not being in the repo, reintroducing the same bug.

This affected everything the Settings page writes, not just API keys:
timezone and a UI-changed login password were being reverted on every
deploy too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 09:34:46 -07:00

106 lines
5.7 KiB
Markdown

# Arr Summary (Greendale)
Flask dashboard aggregating status from the user's self-hosted media stack
— Sonarr, Radarr, SABnzbd, Tautulli, Prowlarr, Ombi — plus Unraid array
stats and a Rotten Tomatoes "what's popular that I don't have yet" feed.
Branded "Media Centre" — a single neutral, professional dark theme (the
earlier five novelty themes were removed). Runs as one Docker container on
the user's Unraid home server. Server-side project directory is still named
`greendale`; the container/repo are named `arr-summary`.
## Architecture
- **`app.py`** — single Flask app, session-based single-shared-password
auth (`LOGIN_PASSWORD` env var, hashed comparison; can be changed at
runtime via `/api/change-password`, persisted to `settings.json`).
Supports running behind a reverse-proxy subpath via `SCRIPT_NAME` +
`ScriptNameMiddleware` + `ProxyFix` (currently deployed at domain root,
not a subpath, but the capability exists — see `nginx/` configs for
both).
- Per-service client functions (`sonarr_get`, `radarr_get`,
`sabnzbd_get`, `tautulli_get`, `prowlarr_get`, `ombi_get`) each call
that service's REST API directly with its API key from env vars.
- The dashboard route (`/`) fetches all services **in parallel** via
`ThreadPoolExecutor` with a hard 5-second wall-clock deadline — any
slow/unreachable service degrades to an error dict rather than
blocking the whole page. This matters for remote/cellular access.
- Aggressive per-service caching with different TTLs tuned to how often
each actually changes: Sonarr/Radarr library lists 5 min (the biggest
latency cost — full Radarr list is ~4.4MB), Tautulli stats 2 min,
SABnzbd history 10 min, Ombi requests 4 hours, Rotten Tomatoes scrapes
4 hours. Active streams (Tautulli) and the SABnzbd queue are always
fetched live, never cached — those need to be current-second accurate.
- Rotten Tomatoes / IMDB are scraped directly (`requests` +
`BeautifulSoup`, JSON-LD parsing), not via an official API — brittle
to upstream HTML changes by nature.
- **`templates/`** — Jinja2 templates (`dashboard.html`, `login.html`,
`settings.html`). Stale duplicates of these once sat at the repo root;
they were never rendered (Flask loads from `templates/`) and were deleted
during the redesign.
- All three share one token set defined on `:root` — surfaces, a 6-step
`--fs-*` type scale, semantic colours, and `--tap: 44px` for minimum
touch targets. Add new UI with those tokens rather than literal values.
- `dashboard.html` is ordered by urgency, deliberately mirroring the cache
tiers in `app.py`: a "Right now" strip (live streams, download queue,
array usage) sits above the fold, with library, discovery, requests and
watch insights below it.
- Degraded services render through the `service_error()` macro, which
pairs `friendly_error` (human cause) with the raw exception behind a
disclosure, passed through `redact_secrets` — SABnzbd and Tautulli put
their API key in the request URL, so it appears in raw tracebacks.
- **`export-to-unraid.sh`** — a pre-existing standalone deploy script
(build image locally → tarball → rsync + `docker load` over SSH) that
predates the Forgejo pipeline below. Still present, not removed, but the
Forgejo pipeline is now the primary deploy path going forward.
- **`nginx/`** — reference nginx location-block snippets for subdomain vs.
subpath reverse-proxy setups; not actually consumed by the app at
runtime (NPM handles proxying in practice), kept as documentation/config
reference.
## Local development
```bash
pip install -r requirements.txt
python3 app.py
```
Runs on port 5000. Needs the various `*_API_KEY` and `*_HOST`/`ARR_HOST`
env vars (see `.env.example`) to reach real services — without them,
dashboard cards degrade to "error" status individually rather than
crashing the page.
## 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`) and runs
`docker compose up -d --build`.
**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.
A key saved through the Settings page overrides the corresponding
`.env` value at startup (see the block after the `*_API_KEY` reads in
`app.py`); `.env` remains the source of truth for anything never
overridden there. Both paths need a container restart to take effect,
since the keys are read once at import.