The footer version is the honest check that a deploy landed, so cut
1.4.0 for the Storage table.
Also record something that has now cost two sessions: the dev machine
isn't on the media stack's network, so every service is offline and the
live-poll endpoints 500 on a timer. That's the normal local baseline,
not a regression. Notes the monkey-patch pattern for exercising a card's
real rendering, plus the two traps that go with it — Jinja's auto_reload
follows app.debug (off), so template edits need a restart rather than a
browser reload, and LOGIN_PASSWORD has to be set explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The GraphQL query only asked for aggregate array fsSize/fsFree, so the
per-disk breakdown added in templates/dashboard.html (commit 5045316)
had no data to render. Now pulls name/device/temp/isSpinning for data
disks, cache disks, and parity disks, and shapes them into the
disks/caches/parities lists the template expects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Indexers card sat beside Requests as a stretched, vertically centred
grid of pills — mostly dead space once the Ombi list was expanded, and it
said nothing about which indexer was actually doing the work.
- Fetch Prowlarr's indexerstats over a rolling 30-day window and merge
per-indexer queries/grabs into the summary. Cached 10 min on success and
never on failure, so a stats outage can't blank the column after it
recovers or take the up/down list down with it.
- Rank indexers busiest-first by grab share (query share as fallback);
queries mostly count enabled indexers, grabs count who delivered.
- One indexer per line, matched to the Ombi request rows: same padding,
gap, radius and two-line body, plus a share bar scaled to the busiest
indexer and an Online/Offline badge the 30s poll keeps current.
- Let that row size cards to their own content instead of stretching.
Bump APP_VERSION to 1.2.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The palette lived in a :root block duplicated across all three
templates, so every reshade meant three edits — and drifted twice in
practice (the favicon hardcoded its own hex; stale danger-border rgba
literals survived two palette changes). Move tokens to a single
static/css/base.css, derive every dim/border tint from the four source
hues with color-mix, and point the favicon at one static/favicon.svg.
Also splits the 1558-line dashboard.html into template + static assets:
662 lines of CSS and 396 of JS were re-sent uninlined on every load
with no browser caching, which costs real time on the remote/cellular
access this app is built for. Templates drop to 501/42/107 lines.
Server-rendered config (timezone, script_root) now arrives via
<body data-*>; wiring URL_BASE to request.script_root also fixes /api/*
fetches for a future subpath mount, which the hardcoded "" would break.
Header service pills now wrap instead of scrolling — they were clipped
at narrow widths, and a hidden pill is a service whose status you
cannot see. Verified 0 clipped and no page overflow at 320/375/700/1280.
Footer gains a version and date so the running build is identifiable
without trusting the Unraid Docker tab's cached container IDs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Every *_API_KEY was read from the env var once at startup and never
checked against settings.json again, so saving a new key on the
Settings page updated the file but had zero effect on real requests —
reported live as "SABnzbd rejected the API key" right after rotating
and saving a new one there. A saved key now overrides the .env value
at startup, matching what the Settings page's own toast already says
("restart the container to apply").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
SABnzbd and Tautulli embed their API key in the request URL, so it
was showing up verbatim in the raw exception text under "Technical
details" on the dashboard — visible to anyone with the study-room
password. Added a redact_secrets filter that strips apikey/token/
password query params before the raw string is rendered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Settings lived inside .header-right, which is hidden below 600px,
leaving no way to reach it on mobile. It's now a standalone 44x44px
button next to the title.
Dashboard error cards showed raw Python/urllib3 exception strings.
Added a friendly_error() filter that classifies the failure (timeout,
connection refused, DNS, auth, 4xx/5xx, TLS) into plain-English copy,
with the raw string still available behind a details disclosure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>