The web app had no auth — anyone reaching the host could browse the library,
trigger downloads, and read/overwrite the stored credentials in Settings. Adds
an opt-in single-shared-password gate:
- Next.js middleware redirects unauthenticated page requests to /login and
returns 401 for /api/*; static assets are excluded via matcher.
- Auth is enabled ONLY when LYRA_PASSWORD is set (unset ⇒ app stays open, so
existing deployments are unchanged). Documented in README "Security".
- The session cookie is httpOnly and carries HMAC-SHA256(password) keyed by
LYRA_SECRET_KEY — unforgeable, and the plaintext password never touches the
client. Edge-safe Web Crypto so one lib/auth.ts serves middleware + route.
- Cookie is NOT Secure: Lyra serves plain HTTP on the LAN; a Secure cookie would
loop. Works behind an HTTPS proxy too.
- /login page + form, a Sign out button (shown only when auth is enabled), and
chrome (nav + worker-status poll) suppressed on /login.
- docker-compose passes LYRA_PASSWORD to web; .env.example documents it.
Verified live end-to-end (built + next start): no-cookie page→307 /login,
API→401, wrong password→401, correct→200+cookie, authed page/API→200, forged
cookie→401, and the auth-disabled (no LYRA_PASSWORD) path stays fully open.
web 133 tests green, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the misleading 6-micro-step progress bar with three honest
phases (Searching -> Downloading -> Finishing). Downloading now shows
a live determinate percentage sourced from job.downloadProgress (Slice
A), with a "NN% · done/total tracks" caption when the track count is
known. Searching and Finishing stay indeterminate.
- status.ts: describeJob collapses to {label, kind}; drop step/totalSteps
and the STAGES array.
- progress-bar.tsx: ProgressBar takes {kind, indeterminate?, percent?,
caption?} instead of step/total/stageLabel.
- job-row.tsx: JobRow takes a `bar?: ReactNode` slot instead of
step/total/indeterminate/stageLabel props.
- queue.tsx: builds the per-state bar + meta; downloading meta drops
the track count (now shown in the bar). Retry button + error note
from the retry feature preserved exactly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an optional lastfmName prop to ArtistModal. When set (only on
/lastfm) it fetches /api/lastfm/artist-plays independently of the
existing preview fetch and renders a most-played-songs list plus a
most-played-albums list above the release grid. Album Want resolves
via /api/mb/release-group then POSTs /api/discover/want, mirroring
the existing want(r) flow. Own-state chips name-match Last.fm albums
against the already-loaded release grid. Other ArtistModal callers
(discover-client.tsx) pass no lastfmName, so their behavior is
unchanged.
- Worker stamps a throttled worker.heartbeat; GET /api/worker reports online
from its updatedAt recency (DB-time); masthead shows a live listening/offline
dot instead of a static decoration.
- /api/requests enriched with rgMbid; Recently Pressed shows cover thumbs.
- Library album count moved from the filter row into a section header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The standalone /discover/artist/[mbid] preview page was an inconsistent
orphan (no art, no tabs, old inline TRACKS toggle). Rebuild it to mirror the
artist-detail page: category tabs, cover thumbnails, and the album modal for
tracks, with per-release Want. Extract shared categoryOf/TAB_ORDER util.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a vinyl-record favicon (icon.svg) — fixes the favicon.ico 404 console
error. Lightweight toast system (ToastHost + toast()) fired on request, wanted
add, search, follow, and want actions so submits give explicit feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking a suggested (or find-similar) artist opens an ArtistModal: Follow +
a cover-art grid of their releases with per-album Want (reuses the preview
endpoint), plus a "Full page" link to the deep-linkable preview route. Clicking
a suggested album opens the AlbumModal (tracklist + Want). Lighter than jumping
to a full preview page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New /library: GET /api/library lists LibraryItems enriched with rgMbid + year
from the matching MonitoredRelease. Client renders a cover-art grid with a live
filter; clicking an album opens the AlbumModal (tracklist). Added Library to the
nav. The album-only overview to see everything downloaded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CoverArt loads album art from the Cover Art Archive CDN by release-group MBID
(browser-side, cached, graceful placeholder fallback). AlbumModal shows cover +
tracklist (fetched from MB) with slots for a status chip and context actions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the remaining pages onto the Pressing Plant system: PageHead
component, shared list-row / toggle / small+ghost button / tracklist classes,
and page wrappers trimmed now that the shell provides masthead + nav. All
logic and test hooks (aria-labels, button text) preserved. Verified in the
real app across both themes (Playwright): artists/discover/wanted/discography
render cohesively with real data.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root layout now wraps every page in the editorial column with a masthead
(wordmark + tagline + live worker dot) and a contents-bar nav with active
state via usePathname. Theme toggle stamps data-theme on <html> and persists
to localStorage. Pages no longer repeat the ad-hoc nav line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>