From 08491c0c9b4e067f437a14f0fde3e43bc67d1d51 Mon Sep 17 00:00:00 2001 From: Barely Removable Date: Sun, 30 Aug 2026 08:05:49 -0700 Subject: [PATCH] Fix System page crash, retire "held" copy, and slim CLAUDE.md - system.html crashed with a 500 whenever SABnzbd history loaded: the trend() macro compared the formatted "avg_speed" string (e.g. "900 KB/s") against a number. Added a real numeric avg_speed_kbps field and pointed the macro at that instead. - Replaced "held"/"not held" wording with "in library"/"not in library" on Home and Discover. - Moved CLAUDE.md's Deployment/reverse-proxy and account-management sections into two on-demand skills (deploy-arr-summary, manage-accounts) so they only load when actually relevant, with a one-line pointer left in their place. - Updated PRODUCT.md's restructure note now that meridian-marquee has merged to main. - Dropped two stale/broken entries from settings.local.json's permission allowlist (a rule from a different machine/path, and one with a corrupted token). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NBw1QodG8ZMYHrGeQfqyDS --- .claude/settings.local.json | 4 +- .claude/skills/deploy-arr-summary/SKILL.md | 83 +++++++++++++++ .claude/skills/manage-accounts/SKILL.md | 39 +++++++ CLAUDE.md | 115 +-------------------- PRODUCT.md | 23 +++-- app.py | 8 +- templates/discover.html | 8 +- templates/home.html | 4 +- templates/system.html | 2 +- 9 files changed, 153 insertions(+), 133 deletions(-) create mode 100644 .claude/skills/deploy-arr-summary/SKILL.md create mode 100644 .claude/skills/manage-accounts/SKILL.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 163a54b..36f0b6c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -21,9 +21,7 @@ "Bash(expect:*)", "Bash(bash export-to-unraid.sh root@192.168.86.33)", "Bash(scp:*)", - "Bash(rsync:*)", - "Bash(/Users/matt/Desktop/summary/arr-summary/gunicorn.conf.py:*)", - "Bash(__NEW_LINE_58e4516f9721fb50__ ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes root@192.168.86.33 \"cd /mnt/user/appdata/greendale && docker compose build --no-cache 2>&1 | tail -3 && docker compose up -d 2>&1\")" + "Bash(rsync:*)" ] } } diff --git a/.claude/skills/deploy-arr-summary/SKILL.md b/.claude/skills/deploy-arr-summary/SKILL.md new file mode 100644 index 0000000..18a9778 --- /dev/null +++ b/.claude/skills/deploy-arr-summary/SKILL.md @@ -0,0 +1,83 @@ +--- +name: deploy-arr-summary +description: Use when deploying Arr Summary (Media Centre), restarting the arr-summary container on Unraid, confirming a deploy landed, or troubleshooting the Nginx Proxy Manager reverse proxy / TLS certs for hippofam.com. Covers the Forgejo push-to-deploy pipeline, manual container restarts, and the DNS-01/Cox port-80 constraints. +--- + +## 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. + +**Confirming a deploy actually landed:** the dashboard footer renders +`v · ` 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: +```bash +openssl s_client -connect :443 -servername hippofam.com +docker exec -it arr-summary python3 manage_users.py passwd +docker exec -it arr-summary python3 manage_users.py remove +``` + +**Adding the first account switches the whole dashboard to per-user +logins and the shared password stops working** — verify you can sign in +as that user before closing the shell. `remove` refuses to delete the +last account for the same reason. + +Users live in `settings.json` rather than an env var deliberately: a +pbkdf2 hash contains `$`, which docker compose interpolates when reading +`.env`. `PASSWORD_HASH_METHOD` is pinned to pbkdf2 rather than Werkzeug's +default scrypt, which needs a hashlib built against OpenSSL and is absent +on the macOS system Python. diff --git a/CLAUDE.md b/CLAUDE.md index 2c2c4ac..a5b4c8d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -166,119 +166,14 @@ app from a shell on an explicit `PORT` is the working path. ## 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. - -**Confirming a deploy actually landed:** the dashboard footer renders -`v · ` 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: -```bash -openssl s_client -connect :443 -servername hippofam.com -docker exec -it arr-summary python3 manage_users.py passwd -docker exec -it arr-summary python3 manage_users.py remove -``` - -**Adding the first account switches the whole dashboard to per-user -logins and the shared password stops working** — verify you can sign in -as that user before closing the shell. `remove` refuses to delete the -last account for the same reason. - -Users live in `settings.json` rather than an env var deliberately: a -pbkdf2 hash contains `$`, which docker compose interpolates when reading -`.env`. `PASSWORD_HASH_METHOD` is pinned to pbkdf2 rather than Werkzeug's -default scrypt, which needs a hashlib built against OpenSSL and is absent -on the macOS system Python. +See the `manage-accounts` skill for the multi-user vs. legacy +single-shared-password login modes and the `manage_users.py` CLI. **Never put a real password in the repo** — not in `app.py`, not in `settings.json` committed to git, not in a fixture. `.env` and `data/` diff --git a/PRODUCT.md b/PRODUCT.md index 60ebd49..b9388d6 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -53,16 +53,19 @@ so this is not optional). Session-based login (single shared password or per-user accounts) gates the whole app; there's no public/unauthenticated view. -**In-progress restructure (branch `meridian-marquee`):** the app is -splitting from one long single-page dashboard into focused pages — Home -("marquee": what's playing, one line of system state, what's new), -Watch, Library, Discover, Requests, System, Activity — each answering one -question well rather than one long scroll trying to show everything. -This also resolves cases where a content-heavy section (e.g. the per-disk -storage list) would otherwise force awkward row heights on a shared -"Right now" strip. `_shell.html` and `_macros.html` are new shared-layout -scaffolding for this split; `dashboard.html` is the outgoing single-page -template being superseded by `home.html` + the per-page templates. +**Multi-page restructure (shipped 2026-08-29, `meridian-marquee` merged +to `main`):** the app split from one long single-page dashboard into +focused pages — Home/P100, Watch, Library, Discover, Requests, System, +Activity — each answering one question well rather than one long scroll +trying to show everything. This also resolved cases where a content-heavy +section (e.g. the per-disk storage list) would otherwise force awkward +row heights on a shared "Right now" strip. `_shell.html` and +`_macros.html` are the shared-layout scaffolding; `dashboard.html` (the +old single-page template) was deleted, superseded by `home.html` + the +per-page templates. Landed together with the visual redesign in +Brand Commitments below — see DESIGN.md and +`static/css/base.css`'s header comment for the visual system's own +invariants. Each page fetches only the services it needs (e.g. `/watch` fetches only Tautulli) rather than the old single route's all-services-at-once fetch — diff --git a/app.py b/app.py index ff2a0b6..37ede9d 100644 --- a/app.py +++ b/app.py @@ -512,7 +512,8 @@ def _get_sab_history_cached(): if duration > 0 and size_bytes > 0: speeds.append(size_bytes / duration) avg_bytes_per_sec = sum(speeds) / len(speeds) if speeds else 0 - avg_speed = _format_speed(avg_bytes_per_sec / 1024) + avg_speed_kbps = avg_bytes_per_sec / 1024 + avg_speed = _format_speed(avg_speed_kbps) # Keep the newest completed items too — "Landed today" on the home # page and the activity feed both read these. Same request as before; # previously only the aggregates survived. @@ -526,12 +527,12 @@ def _get_sab_history_cached(): result = {"week_size": week_size, "day_size": day_size, "completed_7d": completed_7d, "avg_speed": avg_speed, - "recent_items": recent_items} + "avg_speed_kbps": avg_speed_kbps, "recent_items": recent_items} _sab_history_cache = {"data": result, "expires": now + 600} return result except Exception: return {"week_size": "—", "day_size": "—", "completed_7d": "—", - "avg_speed": "—", "recent_items": []} + "avg_speed": "—", "avg_speed_kbps": None, "recent_items": []} def get_sabnzbd_summary(): @@ -565,6 +566,7 @@ def get_sabnzbd_summary(): "week_size": hist["week_size"], "day_size": hist["day_size"], "avg_speed": hist["avg_speed"], + "avg_speed_kbps": hist["avg_speed_kbps"], "recent_items": hist.get("recent_items", []), } except Exception as e: diff --git a/templates/discover.html b/templates/discover.html index 43e21f6..bb00f23 100644 --- a/templates/discover.html +++ b/templates/discover.html @@ -4,11 +4,11 @@ {% block content %}

DISCOVER

-

Popular right now, with anything already held filtered out.

+

Popular right now, with anything already in library filtered out.

Films - {{ rt_movies | length }} not held + {{ rt_movies | length }} not in library
{% if rt_movies %}
@@ -17,12 +17,12 @@ {% endfor %}
{% else %} -

Nothing to suggest — the feed is unreachable or you already hold it all.

+

Nothing to suggest — the feed is unreachable or it's all already in your library.

{% endif %}
Television - {{ rt_tv | length }} not held + {{ rt_tv | length }} not in library
{% if rt_tv %}
diff --git a/templates/home.html b/templates/home.html index b5fcefb..9ad36b2 100644 --- a/templates/home.html +++ b/templates/home.html @@ -27,7 +27,7 @@ 102 Library - Everything already held + Everything in library {% if sonarr.status == 'ok' or radarr.status == 'ok' %} {{ sonarr.total_series | default(0) }} SHOWS  {{ radarr.total_movies | default(0) }} FILMS {% else %} @@ -38,7 +38,7 @@ 103 Discover - Popular, and not yet held + Popular, and not yet in library {{ (rt_movies | length) + (rt_tv | length) }} SUGGESTED diff --git a/templates/system.html b/templates/system.html index 1982e7e..ba0f34d 100644 --- a/templates/system.html +++ b/templates/system.html @@ -120,7 +120,7 @@
In queue
-
{{ sabnzbd.speed | default('0') }} {{ trend(sabnzbd.speed_kbps, sabnzbd.avg_speed) }}
+
{{ sabnzbd.speed | default('0') }} {{ trend(sabnzbd.speed_kbps, sabnzbd.avg_speed_kbps) }}
Speed vs 7-day avg