All checks were successful
Deploy / deploy (push) Successful in 22s
Records what wasn't discoverable from the repo alone after today's outage: Cox blocks inbound port 80 so certs must use DNS-01, the apex proxy host's destination has drifted before, the NPM UI can show a cert as valid/in-use while nginx presents none on disk, and restarting via the Unraid Docker UI throws "No such container" after every deploy because Compose gives the container a fresh ID each time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
148 lines
7.9 KiB
Markdown
148 lines
7.9 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`, `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.
|
|
|
|
## 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.
|
|
|
|
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.
|