Commit Graph

240 Commits

Author SHA1 Message Date
Jonathan de052a4ef7 feat(worker): per-seed discovery contributions so score sums across chunks/sweeps
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:18:30 +02:00
Jonathan bdd8fe72e9 feat: add DiscoverySeedContribution + ScanWorkItem tables 2026-07-13 15:14:58 +02:00
Jonathan db72cab99b fix(web): label Monitor toggle for a11y + correct Settings eyebrow 2026-07-13 15:11:09 +02:00
Jonathan bbe1616ed2 fix(web): harden config PATCH allowlists against prototype-chain keys
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:06:49 +02:00
Jonathan 6decdc7fc9 docs: implementation plan for cleanup batch (Tier A + B)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:04:19 +02:00
Jonathan 785385f0a2 docs: spec for cleanup batch (Tier A loose ends + Tier B design fixes)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:27:51 +02:00
Jonathan e008ee8d13 fix(web): remove inert global autoMonitorFuture checkbox from Monitor tab
The worker never reads the global monitor.autoMonitorFuture Config key;
auto-monitor-future is governed solely by the per-artist
WatchedArtist.autoMonitorFuture toggle on the Artists page. Removes the
misleading no-op checkbox and its DEFAULTS/test entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:08:05 +02:00
Jonathan a67c552f29 feat(worker): chunk the discovery sweep (one chunk per loop iteration)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:57:00 +02:00
Jonathan 5351cd029d feat(worker): discovery chunk_size + seed-count in result (retire max_seeds)
Reconciled the pre-existing test_config_from_config_parses_and_defaults
test in test_discovery.py, which referenced max_seeds/maxSeeds and would
otherwise have broken.
2026-07-13 00:52:08 +02:00
Jonathan df7bf2fcf8 fix(worker): import_album recovers orphaned .old/.importing from an interrupted swap
Adds a crash-recovery preamble that restores `final` from `.old` when a
prior swap was interrupted after rename(final, old) but before
rename(tmp, final), and always drops stale `.old`/`.importing` orphans.

Also guards the swap: if nothing new was staged into tmp (empty/missing
staging) and `final` already exists, leave it untouched instead of
swapping in an empty copy. This is required beyond the literal preamble
because the pre-existing swap logic unconditionally replaced `final`
with `tmp` regardless of whether anything was actually imported, which
would otherwise destroy a just-recovered (or any existing) copy whenever
staging is empty.
2026-07-13 00:46:40 +02:00
Jonathan 44c1b1a62a fix(web): wanted POST converges on P2002 unique race instead of 500
Two concurrent Wants for the same release-group both pass the
findUnique existence check as absent; the loser's create() then hits
the rgMbid unique constraint and threw an uncaught P2002, 500ing the
request. Wrap the create in try/catch and fall back to the same
update-and-return-200 path on P2002, converging instead of erroring.
Non-P2002 errors are rethrown unchanged.
2026-07-13 00:40:29 +02:00
Jonathan 9c793f42d1 fix(web): escape Lucene quotes in searchReleaseGroup query
A `"` or `\` inside artist/album terminates the quoted Lucene phrase
and corrupts the MusicBrainz search query. Escape both terms before
interpolation.
2026-07-13 00:37:00 +02:00
Jonathan fd0bb6aaa0 feat(web): surface discover.listenBrainzUrl; swap maxSeeds->chunkSize in Discovery tab 2026-07-13 00:33:49 +02:00
Jonathan 98735b5353 feat(web): clear-credential buttons on Settings 2026-07-13 00:30:35 +02:00
Jonathan 011852d091 fix(web): close prototype-chain bypass in DELETE /api/config allowlist
FIELDS[field] walked the prototype chain, so field=__proto__ (or
constructor, hasOwnProperty, etc.) resolved to a truthy inherited value
whose .key was undefined. Prisma drops undefined filter values, so
deleteMany({ where: { key: undefined } }) collapsed to deleteMany({}),
wiping the entire Config table. Guard with an own-property check before
indexing FIELDS so only real allowlist entries pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:27:22 +02:00
Jonathan 789eb2c76a feat(web): DELETE /api/config?field= to clear a credential
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:23:21 +02:00
Jonathan e048c68f40 feat(web): Monitor settings tab 2026-07-13 00:20:18 +02:00
Jonathan 2a50c1f518 feat(web): monitor config API route
Implement GET and PATCH endpoints for monitor configuration, matching the existing discover config pattern. GET returns stored or default values; PATCH upserts known keys with allowlist filtering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:16:43 +02:00
Jonathan 34ebe39dd7 docs: implementation plan for deferred-items batch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:12:56 +02:00
Jonathan ab94945f9f docs: spec for deferred-items batch (settings, correctness, chunk discovery)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:07:15 +02:00
Jonathan 05b7090b60 chore: gitignore local ./staging download dir
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:46:06 +02:00
Jonathan e825e3d7ad feat(worker): separate download staging volume (STAGING_DIR)
Staging is no longer forced under the library. The worker reads STAGING_ROOT
(container path, default /music/.staging) and stages per-job downloads there;
docker-compose mounts ${STAGING_DIR:-${MUSIC_DIR}/.staging} at /staging with
STAGING_ROOT=/staging. Set STAGING_DIR to a fast local disk when the library
(MUSIC_DIR) is a network share so temp download I/O stays off the share.

Safe cross-volume: import_album already assembles into a temp dir on the
library volume and swaps atomically there, so partial downloads never touch
the library and the final swap stays atomic. clear_staging_root now clears the
root's contents (mount-safe) rather than removing the root. Documented in
.env.example; new tests cover the separate-root path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:44:27 +02:00
Jonathan 891793953f feat(web): artists page — add-artist modal + filter watched list
The MusicBrainz search+follow moves into a modal opened by an "Add artist"
button; the main view gets a filter that narrows the watched roster (with a
"N of N" count). Follow keeps the modal open (toast per follow) so several can
be added in a row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:22:37 +02:00
Jonathan 606ea82b70 feat: real worker status, covers on Recently Pressed, library counter
- 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>
2026-07-12 17:58:23 +02:00
Jonathan e624d12089 feat(web): rework preview page onto tabs + covers + album modal
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>
2026-07-12 17:58:23 +02:00
Jonathan 92d1345503 feat(web): favicon + toast feedback (polish)
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>
2026-07-12 15:44:24 +02:00
Jonathan c027945191 feat(web): cover art on Wanted + Discover album rows
Wanted API returns rgMbid/type/year; Wanted rows show a cover thumb and open
the album modal. Discover suggested-album rows get a cover thumb too. Completes
cover art across the app (artist detail, Library, modals, Wanted, Discover).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:37:02 +02:00
Jonathan 76eab9e703 feat(web): Settings tabs per service (Qobuz/Soulseek/Library/Discovery)
Split the long Settings page into tabs so each service stands alone, instead
of one long mixed scroll. Same handlers/aria-labels; credential save still
PUTs the full config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:33:20 +02:00
Jonathan 1fbe58013f feat(web): discovery modals — artist + album, not preview navigation
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>
2026-07-12 15:29:46 +02:00
Jonathan 4f6c5995f8 feat(web): Library page — owned-albums grid with cover art
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>
2026-07-12 15:24:16 +02:00
Jonathan d05d092d53 feat(web): artist detail — category tabs, cover thumbs, album modal
Discography now has category tabs (All/Albums/Singles/EPs/Live/Comps/Other
with counts, defaults to Albums), cover-art thumbnails, and clicking an album
opens the AlbumModal with its tracklist. Detail GET gains ?all=true so the
client filters by tab (studio-only default still governs the artists-list
counts). Replaces the old flat date-sorted list + show-all toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:20:06 +02:00
Jonathan 34235a2355 feat(web): CoverArt + reusable AlbumModal
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>
2026-07-12 15:14:34 +02:00
Jonathan 3284059aba feat(web): reusable accessible Modal (portal, focus trap, scroll lock)
Pressing Plant styled modal — ESC + backdrop close, Tab focus trap, body
scroll lock, focus restore, reduced-motion. Foundation for album/artist modals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:12:36 +02:00
Jonathan 3f29ac1930 feat(web): widen app container 900px -> 1120px
More horizontal room for lists, tiles, and the upcoming Library grid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:11:19 +02:00
Jonathan 082365005c fix(worker): reconcile every job + backfill owned releases
Wanted releases pressed while the monitor is off never got reconciled, so
they lingered on the wanted list. Extract finish_job() and call it for every
finished job (not just when monitor.enabled) so the MonitoredRelease
transitions to fulfilled. Add fulfill_owned_releases(), run once at startup,
to backfill releases already owned at/above the quality cutoff (self-heals
items stuck from before this fix). Both idempotent + tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:08:24 +02:00
Jonathan 50949f747b feat(web): restyle Settings + read-only library path panel (Phase 3)
Settings onto the Pressing Plant system: credentials with clear set/unset
affordances, a Library section that shows the configured /music path with a
"change MUSIC_DIR in docker-compose + rebuild" note (honest about the mount
being non-remountable from the UI), restyled scan trigger/result, and the
discovery section. All logic and aria-labels preserved. Completes the
frontend redesign (all pages + shell on one system, both themes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:34:46 +02:00
Jonathan ead5f33bcc feat(web): restyle Artists, Discover, Wanted, discography + preview (Phase 2)
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>
2026-07-12 14:27:43 +02:00
Jonathan 7f1fe52a7c feat(web): rebuild home as "The Floor" on the Pressing Plant system
Home reads /api/requests + /api/wanted + /api/artists: summary tiles
(on the press / pressed / wanted / watching), the on-the-press job list
(state -> stripe + chip + stepped bar via describeJob; needs-attention shows
a literal recovery note), a request form (test hooks preserved), an empty
state, and a recently-pressed deadwax list. Verified in the real app (dark
desktop + mobile); light theme matches the approved comp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:09:42 +02:00
Jonathan fcbe34b027 feat(web): app shell — masthead, contents nav, theme toggle
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>
2026-07-12 14:09:42 +02:00
Jonathan 656e755daf feat(web): Pressing Plant component library + describeJob
design.css component classes (masthead, contents nav, tiles, dept header,
job row + severity stripe, status chip, progress bar, pressed list, buttons,
fields). Presentational _ui components. describeJob maps worker Job.state to
literal labels + severity kind + stepped progress (unit-tested).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:09:30 +02:00
Jonathan e1d96aa603 feat(web): Pressing Plant design tokens + self-hosted Fraunces
globals.css defines the manila/spruce palette as CSS custom properties for
both themes (prefers-color-scheme + explicit data-theme override), paper
grain, base reset, focus-visible, reduced-motion. Fraunces vendored as woff2
and wired via next/font/local with a Palatino/Georgia fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:09:30 +02:00
Jonathan 5f0dcda2ab docs: Pressing Plant frontend Phase 1 implementation plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:57:35 +02:00
Jonathan 4655d815ee docs: Pressing Plant frontend design system spec (Phase 1)
Design language + Phase 1 plan (design system + app shell + home page).
Comp signed off. Liner Notes / record-label identity, chrome-only
metaphor, manila+spruce palette, Fraunces/mono, state-as-form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:53:52 +02:00
Jonathan 8bbc1db32f feat(worker): chunk the library scan + reconnect on DB loss
Two worker-loop robustness fixes:

- Scan chunking: maybe_run_scan advances the library scan by at most one
  scan_chunk (default scan.chunkSize=25 albums) per loop iteration, so a
  large library no longer blocks job-claim/monitor for minutes-to-hours.
  Progress persists in scan.inProgress/scan.cursor/scan.progress and is
  resumable; scan.result (unchanged format) is written on completion.

- DB reconnect: wrap the loop body in `except psycopg.OperationalError`
  (AdminShutdown subclasses it) to close the dead handle and reconnect via
  wait_for_db() in-process, instead of crashing and relying on the
  container restart policy. Verified via a backend-termination test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:37:06 +02:00
Jonathan 62b60e3dae refactor(scan): extract scan_chunk from scan_library
scan_chunk resolves album folders whose (artist, album) sorts after a
cursor, up to a limit, returning (new_cursor, imported, skipped, done).
scan_library becomes the unbounded case (limit=None), so its behavior and
tests are unchanged. Idempotent: all writes are ON CONFLICT, so an
overlapping/repeated cursor is safe. Enables chunked scanning in the loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:36:54 +02:00
Jonathan ade9cd7694 docs: worker robustness design (scan chunking + DB reconnect)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:36:54 +02:00
Jonathan 9ba587d136 fix(worker): run discovery at most once per loop iteration
The scheduled-tick branch and the discover.requested one-shot branch
both read the same stale per-iteration config snapshot, so an enabled+due
sweep coinciding with a pending request ran discovery twice back-to-back
(idempotent but wasteful). Extract maybe_run_discovery, which unifies both
triggers into a single run and is now unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:42:48 +02:00
Jonathan 830de07997 fix(monitor): only auto-monitor core studio releases
autoMonitorFuture flagged any new release, so it could auto-monitor a
newly-released live album or compilation. Gate it on is_core_release so
auto-monitoring matches the studio-only default the UI shows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:42:42 +02:00
Jonathan 33a1ed9cc4 fix(artists-api): create artist + discography atomically
Wrap the WatchedArtist.create + MonitoredRelease.createMany in a
$transaction so a mid-write failure can't leave a followed artist with
no releases (a state the 409 duplicate-follow guard would then wedge).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:42:42 +02:00
Jonathan a07e791368 feat(artists): link live-search hits to the artist preview page
Each MB search result on /artists now links its name to
/discover/artist/[mbid] (the read-only disco + tracklist + own-state
preview), so you can preview before following there too — same wiring
the discovery feed already uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:42:29 +02:00