Credentials page now shows a fact-checked one-line description per
service and marks Spotify/Soulseek/Navidrome as Required (no toggle --
they're load-bearing for every playlist sync). The four optional
integrations (Bandcamp, AzuraCast, Qobuz, Telegram) get a real on/off
switch: Bandcamp pauses its scheduled job (same mechanism as the Jobs
page), the other three just remove their rendered credential file, which
the scripts that read them already treat as "not configured, skip" --
no script changes needed. Disabled optional scopes also drop off the
dashboard's credential status row.
New Settings > Artist Casing page to view/add/remove entries in
artist-canonical.list without shelling in -- adding a name that already
matches case-insensitively replaces its casing in place instead of
duplicating.
Playlist detail page's status section is now two side-by-side columns:
the original Spotify tracklist on the left, and a tabbed Downloaded /
Waiting-to-download view on the right (client-side tabs via Alpine),
both with an internal scroll area so long playlists don't pull the
columns out of sync. Uses the existing status_service reconciliation
(sldl index + beets + quarantine) -- no backend changes needed there.
Vendored htmx.min.js (1.9.12) and alpine.min.js (3.14.1) into static/ --
self-hosted per the no-CDN/CSP requirement, included in base.html.
routers/playlists.py: list/add/remove playlists, a detail page showing
live per-playlist status via status_service (Spotify vs. beets vs. sldl
index vs. quarantine), edit form for active/cron_expr/no_m3u/notes, and a
"Run now" button wired to the jobs run-now endpoint.
routers/credentials.py: write-only credential forms per scope (a saved
secret is never sent back to the browser -- fields just show "(set)" and
leaving a field blank keeps its current value, matching
credential_service.set_credentials' partial-update semantics).
routers/jobs.py: maintenance job list with run-now/enable-disable, and a
recent-runs table that auto-refreshes via HTMX polling
(hx-trigger="every 5s") against a partial-only endpoint -- the one place
in the UI where avoiding a full-page reload actually matters, since job
status changes while the page is sitting open. Per-run log viewing.
Dashboard now shows real data (beets stats, playlist counts, recent job
runs) instead of the placeholder from Task 2.
Verified all 10 authenticated pages render successfully (200, no template
errors) via TestClient with require_auth overridden, including the
playlist detail page's graceful-degradation path when Spotify credentials
aren't configured yet (renders a "couldn't fetch live status" message
instead of a 500).