arr-summary/CLAUDE.md
Barely Removable 2cc62979a2
All checks were successful
Deploy / deploy (push) Successful in 40s
Update CLAUDE.md for the static-asset split and version footer
The architecture notes still described one :root token block shared
inside the three templates, which stopped being true when CSS and JS
moved to static/. Document base.css as the single source of truth, the
four-hue + color-mix rule that replaced hardcoded literals, and the
<body data-*> convention for passing server config to JS.

Also documents the footer version line under the container-restart
section it exists to serve — it is the quickest honest check that a
deploy landed, given the Unraid Docker tab's cached container IDs.

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

9.1 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.
    • Templates carry markup only. CSS and JS live in static/ and are linked via url_for('static', ...) so they stay correct behind a SCRIPT_NAME subpath. Server-rendered config (timezone, script root) reaches JS through <body data-*>, never string-interpolated into a script block.
  • static/css/base.css — the single source of truth for the design tokens, shared by all three templates: surfaces, a 6-step --fs-* type scale, semantic colours, and --tap: 44px for minimum touch targets. Only four hues are declared (--accent, --ok, --warn, --danger); every dim/border tint derives from them via color-mix. Reshading the app should be a four-line edit here — never hardcode a colour literal in a page stylesheet, which is exactly how the palette drifted before (a favicon with its own baked-in hex, and stale rgba() danger borders that survived two palette changes). Page-specific rules live alongside in dashboard.css / login.css / settings.css.
    • 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, 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:

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:

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.

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.