WidgetStore keeps the JSON file (same atomic-write/lock pattern as store.py) and
migrates any old pages.json in place. WidgetData fetches each live widget's own
base_url server-side, normalises Sonarr/Radarr/Plex payloads into one row shape,
caches per <id>@<updated_at> behind UWUDASH_WIDGET_CACHE_TTL, and relays artwork
through /api/widgets/{id}/image so the browser never needs the internal host.
Home now renders one merged calendar timeline, one card per recent widget, then
notices; the Settings table carries a Test button that bypasses the cache.
Pages are a second item type beside services: title + icon + plain-text body,
stored in data/pages.json (PageStore mirrors ServiceStore: lock, atomic temp
+ os.replace, slugify-collision ids) and served by the /api/pages family with
the same admin-write / any-session-read gate.
The viewer area opens on the Home notice board instead of "Nothing selected":
state.selected starts at the HOME sentinel, the startup chain ends in openHome(),
and deleting whatever was being viewed falls back to the board via showHome(),
which repaints without touching which section is active so a delete from the
Settings table leaves Home ready for settingsClose.
PUT is omitted on purpose: the UI only posts/patches and pages have no
non-patchable fields.
Authlib's parse_id_token only reads a 3-segment JWS, so sign-in 500s
against providers that encrypt the id_token (Authentik with an
Encryption Key issues a 5-segment RSA-OAEP-256/A256CBC-HS512 JWE) or
that sign it with the client secret (Authentik with no Signing Key
issues HS256, which the RS256-only JWKS rejects).
OIDCClient subclasses the starlette OAuth2 client and, when the
standard parse fails on a format problem, falls back to an HS256
decode with the client secret, then to the issuer's
userinfo_endpoint. Claim-level failures (bad nonce, expiry, audience,
issuer) are raised as OAuthError so /auth/callback redirects to
/auth/login?error=... instead of blowing up. /auth/authorize likewise
catches provider/network errors and redirects to the login page.
Auth is enabled only when issuer, client id and client secret are all set;
otherwise the dashboard stays an open LAN dashboard. Provider endpoints are
discovered from <issuer>/.well-known/openid-configuration via Authlib, so no
provider-specific code is needed.
- auth.py: /auth/login, /auth/authorize, /auth/callback, /auth/logout (RP-initiated
logout when the issuer advertises end_session_endpoint)
- users.py: atomic, lock-guarded data/users.json keyed by the OIDC sub
- role resolution: admin claim wins, else first user bootstraps as admin, else viewer
- api.py/app.py: session middleware and require_login/require_admin gating on every
route except /health, /auth/* and /static
- config.py/main.py: UWUDASH_OIDC_* / UWUDASH_SESSION_* env vars plus --oidc-issuer
and --public-url flags
- templates/login.html, static app.js/styles.css: sign-in page and settings UI