Commit Graph
100 Commits
Author SHA1 Message Date
bvandeusen 16527cb509 chore: gitignore .claude/ and .remember/
Workspace flatten brought these operator-scoped state dirs (Claude
Code per-machine settings and remember-skill memory artifacts) into
the repo's working tree. Neither belongs in git — the canonical
project memory lives under ~/.claude/projects/ outside the repo.
2026-05-06 10:16:00 -04:00
bvandeusen 3a424c6f73 feat(web/m7-coverage-gauge): coverage API client + query
Adds CoverageRollup type, getCoverageRollup() against
/api/admin/library/coverage, and createCoverageQuery() with the same
3s/30s pacing used by scan-status. New qk.coverage() key follows the
existing zero-arg tuple pattern. Vitest covers the GET path and the
empty-library zero-state plus the with_art + pending + settled = total
invariant.
2026-05-06 10:12:57 -04:00
bvandeusen 97cca4411a chore(server/m7-coverage-gauge): align coverage test with peer style
Code-review polish on admin_coverage_test.go:
- Replace inline artist-seed SQL with the seedArtist helper used
  across the rest of the api test suite (admin_covers_test.go,
  likes_test.go, me_history_test.go, admin_quarantine_test.go).
- Rename the cover_art_source pointer locals from none/sidecar/mbcaa
  to sourceNone/sourceSidecar/sourceMbcaa so they don't read like
  zero-value identifiers.
2026-05-06 10:08:58 -04:00
bvandeusen a1d295ebac feat(server/m7-coverage-gauge): GET /api/admin/library/coverage
New admin handler returns the library-wide cover-art coverage rollup
{total, with_art, pending, settled, pending_no_mbid} for the admin
dashboard gauge. Always 200; zeros on empty library. Same RequireAdmin
middleware as /api/admin/scan/status. Lives under a new /library/
admin sub-namespace, parallel to /scan/ (per-run state) and /covers/
(actions).

Tests gated on MINSTREL_TEST_DATABASE_URL: empty library returns all
zeros, mixed rows produce correct bucket math (verifying the
with_art + pending + settled = total invariant), non-admin returns 403.
2026-05-06 09:54:56 -04:00
bvandeusen ff5868bdab docs(db/m7-coverage-gauge): note CHECK-constraint sync + bucket invariant
Code-review polish on GetAlbumCoverageRollup's comment block:
- Records the invariant that with_art + pending + settled = total
  (and that pending_no_mbid is a subset of pending, not a fourth
  bucket).
- Notes that the IN ('sidecar','embedded','mbcaa') list must stay
  in sync with the cover_art_source CHECK constraint in migration
  0016 — without this note a future source addition could silently
  undercount with_art.
2026-05-06 09:52:10 -04:00
bvandeusen 98ec64c658 feat(db/m7-coverage-gauge): GetAlbumCoverageRollup query
FILTER-aggregate count over albums.cover_art_source and albums.mbid
returning total, with_art, pending, settled, pending_no_mbid in a
single round-trip. Sub-millisecond on realistic libraries; no index
needed for v1. pending_no_mbid is a subset of pending — the UI
tooltip uses it to show how many "pending" rows are blocked on
missing MBID and won't be moved by another scan.

Also bumps the Makefile sqlc pin from 1.27.0 to 1.31.1 to match the
version that produced all existing committed dbq/*.go files. The
prior pin was stale; running make generate against 1.27.0 silently
rolled the codegen back (cosmetic version headers on most files
plus a real codegen shape change in DeleteArtistIfEmpty).
2026-05-06 09:40:26 -04:00
bvandeusenandClaude Opus 4.7 4481874482 fix(server,web/m7-382): handle duplicate-MBID conflicts during backfill
Forward-fix from running the prior two commits in production. Backfill
on a real library exposed two further issues:

(1) gofmt -s flagged the column-aligned stubMeta methods in
    mbids_test.go. Switched to the gofmt-canonical single-space form.

(2) Many albums failed to heal with SQLSTATE 23505 (unique constraint
    albums_mbid_unique). Cause: the operator's library has duplicate
    album rows in the DB — same MusicBrainz release, split into
    multiple rows by the pre-MBID scanner because of subtle title or
    artist disagreements between files. When backfill now correctly
    extracts the MBID, two rows want to claim the same one and the
    partial unique index rejects the second.

    The conflict is correct DB behavior — we shouldn't have two rows
    with the same MBID — but it's not a write failure to alarm the
    operator about. Detect 23505 specifically:
      - downgrade the log line from Warn to Info
      - track these in a new BackfillMBIDsResult.Duplicates counter
        (separate from Skipped, which retains its "no MBID in tag"
        meaning)
      - leave the duplicate row's mbid NULL; merging duplicates is a
        separate (future) operator workflow

    Same handling threaded through the scanner heal path so a regular
    rescan doesn't generate the noise either.

    JSON tally + admin UI gain a "Duplicates" line so the operator
    can see how many duplicate rows their library carries.

Follow-up scope (separate task): a duplicate-album merge UX that
reparents the duplicate's tracks to the canonical row and deletes
the orphaned album row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:45:15 -04:00
bvandeusenandClaude Opus 4.7 cc12ab7e96 fix(server/m7-382): strip NUL bytes from MBIDs before persist
Follow-up to 947f944. With the dhowden/tag/mbz extractor now correctly
matching ID3v2 TXXX frames, mbid backfill started rejecting every row
with "invalid byte sequence for encoding UTF8: 0x00 (SQLSTATE 22021)".

Cause: dhowden's readTextWithDescrFrame (id3v2frames.go:454) — the path
that decodes TXXX text — does NOT strip the trailing/embedded NUL bytes
that ID3v2.4 uses as frame terminator and multi-value separator. Unlike
readTFrame, which does (line 313). So a TXXX:MusicBrainz Album Id of
"abc-123\x00" comes back to us verbatim, and Postgres refuses to store
NULs in text columns.

cleanMBID splits on NUL and returns the first non-empty segment, which
also handles the multi-value case (collaboration artist IDs are
NUL-separated; Minstrel uses the primary for MBCAA, so first wins).

Tests cover the trailing-NUL, multi-value, and all-NUL paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:35:01 -04:00
bvandeusenandClaude Opus 4.7 947f944fe9 fix(server/m7-382): MBID extraction never matched dhowden/tag's Raw() keys
The hand-rolled extractor in internal/library/mbids.go looked up keys that
dhowden/tag never produces, for every supported audio format:

  Vorbis (FLAC/OGG): dhowden lowercases keys at parse time (vorbis.go:59),
    we looked up MUSICBRAINZ_ALBUMID. Always missed.
  ID3v2 (MP3): dhowden stores TXXX frames under bare TXXX/TXXX_N keys
    with *tag.Comm values; the Picard tag name is on the value's
    Description field, not part of the key. We looked up
    TXXX:MusicBrainz Album Id. Always missed.
  MP4 (M4A): dhowden stores freeform iTunes atoms under their bare
    sub-name. We looked up ----:com.apple.iTunes:MusicBrainz Album Id.
    Always missed.

Net effect: every album in the m7-380 boot backfill reported
processed=N, healed=0, skipped=N. The m7-379 enricher gate then
short-circuited NULL on every album, so MBCAA was never queried.
Symptom: "all albums skipped" during cover enrichment.

The pre-existing unit tests passed because they hand-built Raw() maps
with the intended-but-incorrect keys, never round-tripping through
dhowden's parser.

Replace the hand-rolled extractor with dhowden's own mbz sub-package,
which knows the per-format Raw() conventions and uses lowercase
canonical keys (mbz.Album, mbz.Artist). Update both call sites to pass
the full tag.Metadata instead of just Raw(). Rewrite the test with a
stubMeta implementing tag.Metadata, asserting against the actual
per-format Raw() shape, plus a regression guard that pins "uppercase
Vorbis keys must not match" so the v0 bug can't sneak back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:15:51 -04:00
bvandeusen 0db22971c7 fix(server,web/m7-381): forward-fix CI — Mount call + admin.test mock 2026-05-04 20:19:42 -04:00
bvandeusenandClaude Sonnet 4.6 05bc2628a3 feat(web/m7-381): admin overview Library Scan section + manual trigger
Adds getScanStatus/triggerScan helpers, createScanStatusQuery factory
(3s poll), qk.scanStatus(), and a Library Scan section on the admin
overview page with per-stage tallies and inline Run scan button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:16:29 -04:00
bvandeusenandClaude Sonnet 4.6 3d0e213081 feat(server/m7-381): admin scan-status + manual trigger endpoints
Adds GET /api/admin/scan/status and POST /api/admin/scan/run under the
existing RequireAdmin middleware block; plumbs *library.Scanner and
library.RunScanConfig through api.Mount, server.New, and main.go so the
manual trigger reuses the same RunScan orchestrator as startup scans.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:13:31 -04:00
bvandeusenandClaude Sonnet 4.6 65a3c4892c feat(server/m7-381): RunScan orchestrator chains library+backfill+enrich
Adds internal/library/scanrun.go with RunScan, which creates a scan_runs
row and sequences file-walk → MBID backfill → cover enrich, persisting
per-stage tallies as jsonb. Extends coverart.EnrichBatch to return
(processed, succeeded, failed, err) so the orchestrator can classify
outcomes. Replaces main.go's two separate boot goroutines with a single
RunScan call; passes nil scanner in the no-startup-scan branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:08:48 -04:00
bvandeusen 1ed6ca0309 feat(db/m7-381): sqlc queries for scan_runs 2026-05-04 20:04:55 -04:00
bvandeusen 031f4bef8b feat(db/m7-381): scan_runs table for admin scan-status view 2026-05-04 20:04:04 -04:00
bvandeusen 835580ef1e fix(web/m7-380): convert PlaylistTrack to TrackRef in PlaylistCard play handler 2026-05-04 19:46:43 -04:00
bvandeusen b290b4ff0b feat(web/m7-380): animate pending placeholder + add play overlay to PlaylistCard 2026-05-04 19:42:30 -04:00
bvandeusenandClaude Sonnet 4.6 34615fffbd feat(server/m7-380): one-shot MBID backfill worker for existing libraries
Adds a boot-time goroutine that walks albums with NULL mbid, re-reads
tags from one track per album via dhowden/tag, and persists album + artist
MBIDs. Healed albums also get their cover_art_source='none' cleared so the
enricher's next batch retries the MBCAA fetch. Caps at 5000 albums per
boot to avoid stalling startup on huge libraries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 19:40:27 -04:00
bvandeusen 26f4c7a79f fix(web/m7-352): use typographic apostrophe in failed-variant copy 2026-05-04 19:16:25 -04:00
bvandeusen b8f9b3891b fix(server/m7-379): enricher leaves cover_art_source NULL when MBID missing 2026-05-04 19:04:16 -04:00
bvandeusen 6c6c4bc1e4 feat(server/m7-379): heal existing artist+album mbid on rescan 2026-05-04 19:03:12 -04:00
bvandeusenandClaude Sonnet 4.6 ab8fff834a feat(server/m7-379): extract MusicBrainz IDs from audio tags during scan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 19:01:55 -04:00
bvandeusen 412a1c00ac fix(web/m7-352): update listPlaylists test URL after kind filter addition 2026-05-04 18:52:40 -04:00
bvandeusenandClaude Sonnet 4.6 5698cfe8e4 feat(web/m7-352): home Playlists row — For-You + Songs-like + user playlists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 18:34:03 -04:00
bvandeusen b8bb1c1fa7 feat(web/m7-352): PlaylistPlaceholderCard with 4 state variants 2026-05-04 18:31:42 -04:00
bvandeusen 886903ae27 feat(web/m7-352): kind-aware createPlaylistsQuery + system-playlists-status helper 2026-05-04 18:30:43 -04:00
bvandeusen 56f0998f84 feat(web/m7-352): Playlist type gains kind/system_variant/seed_artist_id fields 2026-05-04 18:14:46 -04:00
bvandeusen a8630a1355 feat(server/m7-352): GET /api/me/system-playlists-status for home placeholders 2026-05-04 18:13:13 -04:00
bvandeusen db361c8305 feat(web/m7-377): CompactTrackCard gains like + queue + kebab overlays 2026-05-04 17:49:19 -04:00
bvandeusen a062c8229f fix(server/m7-353): rename unused r in handleAdminBulkRefetchCovers 2026-05-04 17:43:22 -04:00
bvandeusen 1691290875 fix(server): forward-fix CI — gofmt convert/types + rename unused r in enricher_test 2026-05-04 17:41:16 -04:00
bvandeusenandClaude Opus 4.7 0dbe326d8b fix(server,web): forward-fix CI lint + vitest failures
- gofmt -s on system.go, system_cron.go, api.go
- rename unused r → _ in 3 fetcher_test.go HTTP handlers
- TrackRow +queue test uses /add .* to queue/i (track-aware aria-label from #377)
- /library/artists page test mocks likes + tanstack-query (ArtistCard now embeds LikeButton)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:39:06 -04:00
bvandeusen 93f54e00a4 fix(server,web/m7-353): forward-fix CI — Mount call + AlbumRef test fixtures 2026-05-04 17:31:27 -04:00
bvandeusenandClaude Sonnet 4.6 4e3bd46d69 fix(server,web/m7-353): post-review fixes (test compilation, AlbumRef field, bulk handler, design system)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:21:24 -04:00
bvandeusenandClaude Sonnet 4.6 28617df5bd feat(web/m7-353): admin cover-refetch UI (per-album + bulk)
Adds cover_art_source to AlbumRef, two admin API helpers (refetchAlbumCover,
refetchMissingCovers), a per-album retry button gated on is_admin in the album
detail page, and a bulk-refetch section in the admin overview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:15:26 -04:00
bvandeusenandClaude Sonnet 4.6 d4a837b904 feat(server/m7-353): admin endpoints for cover refetch (per-album + bulk)
Adds POST /api/admin/albums/{id}/cover/refetch (synchronous single-album
retry) and POST /api/admin/covers/refetch-missing (async bulk drain).
Wires coverart.Enricher through server.New → api.Mount → handlers struct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:11:53 -04:00
bvandeusen 6c33acf062 feat(server/m7-353): config + main.go wiring for coverart enricher 2026-05-04 15:06:28 -04:00
bvandeusen 64475d45dc fix(server/m7-353): log retry-missing errors + tighten 404 test + cover EnrichRetryMissing 2026-05-04 15:04:23 -04:00
bvandeusenandClaude Sonnet 4.6 78994a60cf feat(server/m7-353): coverart.Enricher orchestrates sidecar + MBCAA + record
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:00:28 -04:00
bvandeusen 2d3841966e feat(server/m7-353): coverart.Fetcher MBCAA HTTP client + rate limiter 2026-05-04 14:49:28 -04:00
bvandeusen ef6c09dbfb refactor(server/m7-353): consolidate findSidecarCover into coverart package 2026-05-04 14:46:28 -04:00
bvandeusen 70defdb5bc feat(server/m7-353): coverart.FindSidecar shared helper + tests 2026-05-04 14:45:20 -04:00
bvandeusen 15170ed7c6 feat(db/m7-353): sqlc queries for album cover enrichment 2026-05-04 14:30:55 -04:00
bvandeusen 458a183099 feat(db/m7-353): add albums.cover_art_source enum column 2026-05-04 14:29:13 -04:00
bvandeusenandClaude Sonnet 4.6 40be3bf917 feat(web/m7-377): album detail header gets LikeButton + Play/Shuffle row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 13:10:47 -04:00
bvandeusen ebafdbfb05 feat(web/m7-377): artist detail moves LikeButton inline with heading at size lg 2026-05-04 13:09:25 -04:00
bvandeusen 8fff7f62d6 refactor(web/m7-377): TrackRow +queue uses Lucide Plus icon + scoped aria-label 2026-05-04 13:08:26 -04:00
bvandeusen d35ed5945a feat(web/m7-377): AlbumCard gains bottom-right kebab 2026-05-04 13:07:06 -04:00
bvandeusen 4e42fc8280 feat(web/m7-377): ArtistCard top-right cluster + bottom-right kebab 2026-05-04 13:05:11 -04:00
bvandeusenandClaude Sonnet 4.6 43a3dc47a8 feat(web/m7-377): AlbumMenu component + listAlbumTracks helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 11:59:38 -04:00
bvandeusenandClaude Sonnet 4.6 ce01b3d992 feat(web/m7-377): ArtistMenu component with like/queue/playlist actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 11:53:41 -04:00
bvandeusen 72496852a5 feat(web/m7-377): LikeButton gains size prop (sm/md/lg) 2026-05-04 11:50:31 -04:00
bvandeusen 9eeb79e99f fix(web/m7-377): update AddToPlaylistMenu tests for tracks-array prop 2026-05-04 11:48:55 -04:00
bvandeusen 7ddf77ace5 refactor(web/m7-377): AddToPlaylistMenu accepts tracks: TrackRef[] 2026-05-04 11:47:53 -04:00
bvandeusen 2b286a9bfd docs(db/m7-352): note ListPlaylistsByUserAndKind is test-only
Production list-playlists handler reuses Service.List + in-memory kind
filter so other users' public playlists can surface alongside the
caller's filtered own. Comment clarifies the divergence so future
readers don't expect this query to be wired into the API path.
2026-05-04 10:31:09 -04:00
bvandeusen 0a1e76564d feat(server/m7-352): 403 system_playlist_readonly on playlist edit endpoints
Push kind='system' guard into each of the five Service edit methods
(Update, Delete, AppendTracks, RemoveTrack, Reorder) immediately after
the ownership check; map the typed error to 403 in writePlaylistErr;
add table-driven test covering all five verbs against a seeded system
playlist.
2026-05-04 09:29:37 -04:00
bvandeusenandClaude Sonnet 4.6 a90ff11f0f feat(server/m7-352): kind filter on GET /api/playlists + lazy fallback
Adds ?kind= filter (user|system|all, default user) to GET /api/playlists
so system mixes don't leak into slice-1 SPA views. Lazy background build
fires when kind=system|all and the caller's run row is stale (>24h).
Propagates Kind/SystemVariant/SeedArtistID through PlaylistRow and
playlistRowView wire types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 09:24:27 -04:00
bvandeusen 391f5f16a1 feat(server/m7-352): cron loop + main.go wiring for system playlist build 2026-05-04 09:16:07 -04:00
bvandeusen 797a2c8a45 fix(server/m7-352): log Finish/Fail run errors; drop unused seed-limit constant 2026-05-04 09:14:40 -04:00
bvandeusenandClaude Sonnet 4.6 46a9de8e9a feat(server/m7-352): BuildSystemPlaylists with atomic replace + concurrency guard
Implements T5 of #352 slice 2: adds CreateSystemPlaylist sqlc query,
BuildSystemPlaylists function (For-You + Songs-like mixes, tx atomic
replace, in_flight guard, tieBreakHash determinism), and 7 integration
tests covering activity, quarantine exclusion, atomic replace, concurrency,
daily nonce stability, ListActiveUsers, and stale-in-flight recovery.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 08:35:02 -04:00
bvandeusen ef478778ef feat(server/m7-352): system mix helpers (tieBreakHash, pickSeedArtists) 2026-05-04 08:24:39 -04:00
bvandeusen d7aebafa5d feat(server/m7-352): AssertEditable + ErrSystemPlaylistReadonly on playlists service 2026-05-04 08:21:39 -04:00
bvandeusen 58a27e2f0f fix(db/m7-352): cast COALESCE to uuid so sqlc emits typed return 2026-05-04 08:15:21 -04:00
bvandeusenandClaude Sonnet 4.6 3fe929cebd feat(db/m7-352): sqlc queries for system playlist runs + seed selection
Adds internal/db/queries/system_playlists.sql with 12 named queries
covering active-user enumeration, per-user run tracking (claim/finish/fail),
seed-artist/track/cover selection, and playlist CRUD by kind.
Runs sqlc generate to emit dbq/system_playlists.sql.go; also updates
playlists.sql.go and models.go to reflect the new kind/system_variant/
seed_artist_id columns added in migration 0015.

Note: plan specified a.cover_path for PickTopAlbumCoverForArtistByUser
but albums uses cover_art_path — corrected in the query file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 08:14:20 -04:00
bvandeusen 36d05310ce feat(db/m7-352): system playlists schema (kind, variant, seed_artist_id, runs table) 2026-05-04 08:03:06 -04:00
bvandeusen 79d6045f7a fix(server/m7-365): silence unused-pool-param lint in history test 2026-05-04 07:05:49 -04:00
bvandeusen aa5b277b6b fix(web/m7-365): history page error-state test asserts by role 2026-05-04 07:05:20 -04:00
bvandeusen c04b288737 feat(web/m7-365): add History entry to Library nav 2026-05-04 06:59:05 -04:00
bvandeusen cd0b9c97e1 fix(web/m7-365): align /library/history with albums-page conventions
Code-quality review flagged convention drift from sibling library
pages. Aligning so the history page matches the same shape:

- queryStore + $derived($queryStore) pattern (single subscription
  point) instead of $query.X everywhere.
- h1 uses font-display text-2xl font-medium per FabledSword design
  system (weights 400/500 only — font-semibold drifted to 600).
- h2 uses font-medium + z-10 for sticky-header layering.
- InfiniteScrollSentinel uses its `enabled` prop (which exists; the
  earlier spec note claiming otherwise was wrong) so the observer
  isn't recreated on every fetch cycle.
- Loading more… / End of history footers added to match albums.
- onRetry passes query.refetch by reference, not wrapped.
- Dropped redundant `as HistoryEvent[]` cast.

Also fixed test case 4 which trivially passed regardless of the
conditional gate's correctness — now queries the sentinel's actual
DOM root (div[aria-hidden="true"].h-px) and asserts presence/absence.
Added a positive twin test for hasNextPage=true.
2026-05-04 06:57:24 -04:00
bvandeusenandClaude Sonnet 4.6 ecf4ed86f5 feat(web/m7-365): /library/history page with day grouping + infinite scroll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 06:53:37 -04:00
bvandeusen da39ff847b feat(web/m7-365): HistoryRow component with click-to-play 2026-05-04 06:19:59 -04:00
bvandeusen 4dee5922d9 feat(web/m7-365): dayBucket + groupByDay utilities 2026-05-04 06:18:18 -04:00
bvandeusen 46066c4d08 feat(web/m7-365): history API helper + tests 2026-05-04 06:16:15 -04:00
bvandeusenandClaude Sonnet 4.6 fc608fb36e feat(server/m7-365): GET /api/me/history handler + integration test
Implements the listening-history endpoint for M7 #365: returns a user's
play events newest-first, excluding skipped events and quarantined tracks,
with offset/limit pagination and a has_more heuristic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 05:52:17 -04:00
bvandeusen 45a17e4e95 feat(server/m7-365): sqlc query for user listening history 2026-05-04 05:46:30 -04:00
bvandeusen d202319c87 fix(web/m7-364): vitest failures — exact-match query, inert prop, -0, user guard
CI test-web was blocking on:

- QueueDrawer.test close-button query: getByLabelText(/close queue/i)
  matched BOTH the backdrop button (aria-label="Close queue backdrop")
  and the close button (aria-label="Close queue"). Switched to exact
  string match.

- QueueDrawer.test inert assertion: Svelte 5 + jsdom uses property
  binding for `inert`, not attribute, so hasAttribute('inert') returns
  false. Check the DOM property (with attribute fallback for
  robustness) instead.

- queue-row-math.ts -0 normalization: Math.round(-0.5) returns -0,
  and Object.is(-0, 0) is false — vitest's .toBe(0) fails. Added
  `|| 0` to normalize -0 to +0 at the function boundary so consumers
  never see -0.

- player/store.svelte.ts user import guard: persistence $effect.root
  reads `user.value?.id` at module-init, but in test files where
  auth/store.svelte.ts is imported transitively (auth/store.test,
  playlists/playlists.test), the player module loads via auth's
  import chain before auth's `user` binding is assigned. `user?.value`
  guards against that init-order race. The proper fix is to invert
  the auth↔player import dep (filed as I2 follow-up under #375).
2026-05-03 22:37:00 -04:00
bvandeusen d43c6b31f4 chore(web/lint): clear svelte-check warnings + fix QueueDrawer test query
CI svelte-check was blocking on:

- 1 ERROR I introduced in the previous cleanup: QueueDrawer.test.ts
  passed { hidden: true } to getByLabelText, but that option only
  exists on getByRole. Fixed by switching to a direct
  document.querySelector for the aria-hidden assertion.

- 13 WARNINGS pre-existing in the codebase from M7 #352/#372/#349
  era, never surfaced because earlier CI runs failed before reaching
  type-check. Now that CI gets that far, they accumulate. Cleared:

  - FlagPopover, RemoveTrackPopover, AddToPlaylistMenu, TrackMenu:
    interactive role divs gain tabindex="-1" + onkeydown that stops
    propagation and closes on Escape (functional, not just warning-
    suppression).

  - FlagPopover state-from-props: $state(untrack(() => prop ?? default))
    for explicit initial-snapshot semantics; const isUpdate switched
    to $derived so it reacts to prop changes.

  - PlaylistTrackRow drag-div: role="listitem" added.

  - playlists/+page.svelte: autofocus replaced with bind:this + $effect.
2026-05-03 22:26:09 -04:00
bvandeusen 22e3f67411 feat(web/m7-364): drawer focus management + grip Enter/Space handling 2026-05-03 22:23:20 -04:00
bvandeusenandClaude Sonnet 4.6 d83b3eab25 fix(web/m7-364): TrackRef field shape + seek-on-restore + drawer inert
CI svelte-check failed on 5 type errors caused by the slice using
duration_ms/album_name fields that don't exist on TrackRef (the real
shape uses duration_sec + album_id/album_title). Fixed across
QueueDrawer, QueueDrawer.test, QueueTrackRow.test, persisted.test,
and the inline `state.current = null` in PlayerBar.test (TrackRef |
undefined, not | null).

Final whole-slice review concerns also rolled in:

- C1 (Critical): persisted position was restored to UI but never
  seeked into the audio element — first timeupdate snapped _position
  back to 0. Added a one-shot _pendingRestorePosition module ref +
  consumePendingRestorePosition() export; layout's loadedmetadata
  handler now seeks once on restore.

- I1 (Important): throttled-write effect now wraps _queue / _index
  reads in untrack so the dependency tracking matches the design
  intent. Position remains the only tracked dep.

- I4 (Important): drawer gets inert={!queueDrawerOpen} so its inner
  buttons drop out of tab order when closed (aria-hidden alone
  doesn't do that). Removed redundant role="complementary" from
  <aside> (implied by the element). New test asserts inert binding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 22:14:59 -04:00
bvandeusen 49c8e5959e feat(web/m7-364): mount QueueDrawer + esc handler + restore on bootstrap 2026-05-03 21:53:36 -04:00
bvandeusen 82846c9fcd fix(web/m7-364): align queue toggle styling with sibling toggles
- Queue toggle active class: bg-accent-tint text-accent (matches shuffle/repeat)
- ListMusic icon size: 20 → 18 (matches siblings)
- Right-side container width: w-48 → w-60 (accommodates 4 buttons + volume)
2026-05-03 21:52:39 -04:00
bvandeusenandClaude Sonnet 4.6 36805914d8 feat(web/m7-364): PlayerBar queue toggle + Up next line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:50:14 -04:00
bvandeusen b99eb5f120 feat(web/m7-364): QueueDrawer side-panel with track list 2026-05-03 21:47:33 -04:00
bvandeusen e164d69492 fix(web/m7-364): keyboard reorder + measured row height + offsetToDelta helper 2026-05-03 21:45:32 -04:00
bvandeusen 57a353a138 feat(web/m7-364): QueueTrackRow with neodrag reorder + remove 2026-05-03 21:41:59 -04:00
bvandeusen b2e3913df8 feat(web/m7-364): clear persisted queue + reset queue on logout 2026-05-03 21:39:57 -04:00
bvandeusenandClaude Sonnet 4.6 472b9d9e84 fix(web/m7-364): untrack _position in queue immediate-write effect
Wrap the _position read inside untrack() in the immediate-write $effect
so it no longer registers as a reactive dependency, preventing the effect
from firing on every 4Hz position tick. Also adds three missing
restoreQueue unit tests with vi.mock for the auth store.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:37:55 -04:00
bvandeusen 8cc79ee612 feat(web/m7-364): queue persistence write/restore on player store 2026-05-03 20:44:33 -04:00
bvandeusen bde017dad3 fix(web/m7-364): pause on last-track remove + reset drawer in test setup 2026-05-03 20:37:33 -04:00
bvandeusenandClaude Sonnet 4.6 6f9b5d0059 feat(web/m7-364): queue mutations + drawer state on player store
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 20:29:48 -04:00
bvandeusen 8053704afa feat(web/m7-364): localStorage persistence layer for queue 2026-05-03 20:26:07 -04:00
bvandeusen ac44713ec3 chore(web/m7-364): add @neodrag/svelte for queue reorder 2026-05-03 20:24:25 -04:00
bvandeusen 180d85eec0 chore(web): post-#363 cleanup — DRY tripwires, drop unused description, singular playlist title
M2 — Add TODO(#375) tripwire comments where the dark/light theme-color
hex pair is duplicated outside tokens.json (vite.config.ts and
applyMetaThemeColor.svelte.ts). Refactoring is out of scope for #363;
the comments are stop signs for the next person who edits these.

M5 — The SPA never reads window.__MINSTREL__.description. The OG meta
description is server-rendered and complete on its own. Drop the dead
inline-script field and the corresponding helpers in branding.ts.
Server-side BrandingConfig.Description stays — it's still used for
<meta name="description"> and og:description.

Copy nit — Playlist detail page title becomes "Playlist · Foo"
matching the singular form already used by Artist · / Album ·.
2026-05-03 19:04:59 -04:00
bvandeusen a8fd33d4ed chore(lint): clear pre-existing CI lint debt
Nine golangci-lint failures accumulated across M7 #352, #372, and
6d1709c that were never surfaced because test-go never ran green long
enough to reach the lint step. The first push to land cleanly through
vet (the M7 #363 slice) ran lint and exposed them.

- errcheck: discard the error on defer Close() in collage.go,
  collage_test.go, and server_test.go.
- gofmt -s: re-run on config.go and playlists_test.go.
- goimports: move the stray "time" import from the local-module group
  to stdlib in server_test.go.
- revive unused-parameter: rename ctx to _ on stubScanner.Scan and
  fakeLidarrUnmonitorer.UnmonitorTrack (test stubs); on
  tracks.Service's adminID, add //nolint:revive directive rather than
  renaming because the HTTP handler passes admin.ID (a real authed-user
  UUID) and the existing comment already flags it as reserved for the
  audit-log follow-up.
2026-05-03 18:59:58 -04:00
bvandeusenandClaude Sonnet 4.6 66f651cf5c fix(web/m7-362): repair Vitest fixtures (matchMedia.fire, listenbrainz mock)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 18:48:20 -04:00
bvandeusen f8684f0034 fix(server,web): update test fixtures for new New/Handler signatures 2026-05-03 18:44:34 -04:00
bvandeusenandClaude Sonnet 4.6 ca82862346 chore(config): rename SMARTMUSIC_ env prefix to MINSTREL_; default admin user
The legacy SMARTMUSIC_ prefix dates from when the project was specced
as "smart-music" before being renamed to Minstrel. Hard cutover to
MINSTREL_* across config.go, tests, config.example.yaml, README, and
docker-compose. Also renames SMARTMUSIC_CONFIG in cmd/minstrel/main.go
which was missed in the original audit.

Also seeds Auth.AdminBootstrap.Username = "admin" in Default() so
MINSTREL_DATABASE_URL is the only env var required for a fresh prod
deployment — the bootstrap auto-generates a password and prints it
to stderr. Drops the now-redundant MINSTREL_AUTH_ADMIN_USERNAME=admin
line from the README quickstart and docker-compose.

M1 follow-up from the #363 final review: guards
MINSTREL_BRANDING_APP_NAME and _DESCRIPTION against empty-string
overrides — empty would have shipped <title></title> to share-preview
crawlers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 18:40:59 -04:00
bvandeusen 841b4cd8df fix(docs/m7-363): README env vars + first-run instructions match reality 2026-05-03 17:54:03 -04:00
bvandeusen 874fe17191 docs(m7-363): operator-first README rewrite 2026-05-03 17:50:17 -04:00
bvandeusen 574bf30504 feat(web/m7-363): OG share-image placeholder + one-off generator 2026-05-03 17:47:05 -04:00