Replaces the five novelty themes with one professional dark theme built on a shared token set (surfaces, 6-step type scale, semantic colours, 44px tap target) used across dashboard, login and settings. Layout now matches the cache tiers in app.py: a "Right now" strip with live streams, the download queue and array usage sits above the fold, with library, discovery, requests and watch insights below. Previously the page opened on rarely-actionable disk stats and buried active streams under twenty recommendation rows. Also from the design review: - Discover lists show 5 with a Show more toggle instead of 10 each - RT descriptions render inline and clamped, not hover-only tooltips that touch devices could never reach - Adding a title is now two-step (tap to arm, tap to confirm) since it queues a real download - role=alert on errors, aria-live on polled regions, semantic dl/dt/dd stats, labelled search inputs - every text/surface pair verified at >= 5:1 contrast Drops the dead delete-modal JS, which threw a TypeError on every load, and the stale root-level template copies that Flask never rendered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.9 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_PASSWORDenv var, hashed comparison; can be changed at runtime via/api/change-password, persisted tosettings.json). Supports running behind a reverse-proxy subpath viaSCRIPT_NAME+ScriptNameMiddleware+ProxyFix(currently deployed at domain root, not a subpath, but the capability exists — seenginx/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 viaThreadPoolExecutorwith 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.
- Per-service client functions (
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 fromtemplates/) 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: 44pxfor minimum touch targets. Add new UI with those tokens rather than literal values. dashboard.htmlis ordered by urgency, deliberately mirroring the cache tiers inapp.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 pairsfriendly_error(human cause) with the raw exception behind a disclosure, passed throughredact_secrets— SABnzbd and Tautulli put their API key in the request URL, so it appears in raw tracebacks.
- All three share one token set defined on
export-to-unraid.sh— a pre-existing standalone deploy script (build image locally → tarball → rsync +docker loadover 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.
settings.json (timezone, changed login password) also persists
server-side outside git.