The dashboard had one shared password and no concept of a user. This
adds real accounts: a username field on the login page, per-user
credentials, and the signed-in user recorded on the session, which is
the groundwork for per-user permissions later.
Credentials are salted pbkdf2 hashes in settings.json's `users` map.
Three deliberate choices there:
- settings.json rather than an env var, because a pbkdf2 hash contains
'$' and docker compose interpolates that when it reads .env. It's also
already the bind-mounted file that survives `up -d --build`.
- pbkdf2 rather than Werkzeug's default scrypt, which needs a hashlib
built against OpenSSL — the macOS system Python has no hashlib.scrypt
and would raise at hash time.
- load_users() re-reads the file per check, so adding a user or changing
a password is live in every gunicorn worker with no restart. The old
_runtime_password was per-worker and only ever half-applied.
Multi-user activates only once an account exists. Until then the
original LOGIN_PASSWORD path is used unchanged and the username field is
hidden, so deploying this can't lock the existing install out.
manage_users.py prompts for passwords via getpass — never echoed, never
a CLI argument — and only ever writes hashes; it refuses to remove the
last account, which would silently drop the dashboard back to the shared
password.
Two fixes carried along: login no longer requires the LOGIN_PASSWORD env
var specifically, so a settings.json-only password works instead of
locking you out; and the legacy comparison now uses hmac.compare_digest,
which the old docstring claimed but '==' didn't provide.
Also gitignores settings.json and data/ — they hold API keys and now
password hashes, were excluded from the deploy rsync but not from git.
Verified: legacy fallback still signs in with no accounts configured; a
9-case credential matrix (right/wrong user x right/wrong password, plus
the retired shared password); per-user change-password scoped to the
signed-in account and live without restart; and the add/passwd/remove
guards.
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>
Switch from the warm graphite/orange theme to a cooler charcoal/indigo
palette with a red (not pink/purple) danger color, and restyle error
banners to sit on the normal card surface with an accent stripe instead
of washing the whole box in color. Also fix the Requests/Indexers row
stretching the Prowlarr card to match Ombi's longer request list,
leaving a large blank area under the indexer grid.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the slate/indigo palette with a warmer graphite base and an
orange accent (#e07a35), per user's pick from a side-by-side comparison
of five directions. Same token structure, same contrast bar (all pairs
still >= 4.5:1) — only the hex values changed, across dashboard, login
and settings plus the three embedded favicons.
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>