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

5.7 KiB

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

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.