Commit Graph

420 Commits

Author SHA1 Message Date
bvandeusen 4e47b2e7f5 feat(web): pair scroll arrows beside section title; bleed scroller to viewport edge
Restructures HorizontalScrollRow so paired arrows sit in a flex header
beside an optional section title (instead of absolute-positioned at the
row's left/right edges). Adds margin-right: -1rem to the row so the
scroller's items extend to the viewport's right edge, escaping Shell's
p-4 padding. The header bar keeps its right gutter so titles align with
the rest of the page content.

Home page passes 'title' to the first row of each section; subsequent
rows in multi-row sections render arrow-only headers.
2026-05-01 21:05:16 -04:00
bvandeusen 4412a4af0c feat(web): update nav (Home/Artists/Albums); retire ArtistRow
Add Artists and Albums nav entries, rename root label from Library to
Home. Migrate all three ArtistRow call sites (search, search/artists,
library/liked) to ArtistCard with grid wrapper; delete ArtistRow
component and its test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:27:53 -04:00
bvandeusen 3d0a42865e feat(web): add /library/albums wrapping-grid page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:26:02 -04:00
bvandeusen dae08cd71a feat(web): add /library/artists wrapping-grid page
Implements Task 18: AlphabeticalGrid + ArtistCard infinite-scroll page
backed by createArtistsQuery('alpha'), with error, empty, and loading states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:24:48 -04:00
bvandeusen 7d09e605cc feat(web): rewrite / as home page composing 4 horizontal-scroll sections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:23:20 -04:00
bvandeusen cf18d51394 feat(web): add AlphabeticalGrid wrapper with letter dividers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:21:34 -04:00
bvandeusen 0586483a42 feat(web): add CompactTrackCard for Most played rows
Small 140px clickable card showing album cover, title, and artist name.
Clicking calls playQueue(sectionTracks, index) to play through the full
section list. Includes Vitest tests for click and render behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:19:16 -04:00
bvandeusen c7e7dd269b feat(web): add ArtistCard (circular) with play-shuffle overlay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:18:05 -04:00
bvandeusen b5741e59b7 feat(web): polish AlbumCard with FabledSword tokens + play overlay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:16:43 -04:00
bvandeusen 8f5eb21094 feat(web): add HorizontalScrollRow component
Reusable horizontally-scrolling row with left/right arrow buttons,
scroll-boundary disabling, snap scrolling, and hover-reveal arrows.
Used by M6a home page (Task 17).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:15:10 -04:00
bvandeusen e674869e06 feat(web): add home/albums/artists API clients for M6a
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:13:49 -04:00
bvandeusen b1f2227d62 feat(web): extend ArtistRef/AlbumRef + add HomePayload + new query keys
Adds sort_name/cover_url to ArtistRef, sort_title to AlbumRef (matching
backend Task 5 wire shape), HomePayload type, and qk.home/albumsAlpha/
artistTracks query keys. Updates existing test fixtures to satisfy the
new required fields.

Verified clean via 'npm run check' (0 errors, only pre-existing warnings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:12:25 -04:00
bvandeusen c5e42ec8c2 feat(api): wire M6a routes; alpha artist list uses covers query
Replace handleListArtists alpha branch with ListArtistsAlphaWithCovers
(drops N+1 per-artist album lookup). Register /api/home and
/api/library/albums in api.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:10:04 -04:00
bvandeusen 2aecbba2ef feat(api): add GET /api/artists/{id}/tracks handler
Wires ListArtistTracksForUser (Task 3 SQL) to a new handler that returns
all quarantine-filtered tracks for an artist as a flat TrackRef list.
404 when the artist doesn't exist. Route registered in Mount().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:06:15 -04:00
bvandeusen fc12532dee feat(api): add GET /api/library/albums handler
Implements the paged alpha-sorted album list endpoint used by the M6a
wrapping-grid SPA page. Mirrors handleListArtists alpha branch using
ListAlbumsAlphaWithArtist + CountAlbums; 2/2 integration tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:00:59 -04:00
bvandeusen 00804cb974 feat(api): add GET /api/home composite handler
Wires recommendation.HomeData into handleGetHome; maps dbq row types to
the five HomePayload wire-type slices (AlbumRef/ArtistRef/TrackRef).
All slices are non-nil so the SPA never receives JSON null for empty sections.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 17:58:59 -04:00
bvandeusen 1abaf15051 feat(api): extend ArtistRef/AlbumRef + add HomePayload for M6a
- ArtistRef gains SortName and CoverURL fields
- AlbumRef gains SortTitle field
- albumRefFrom now populates SortTitle from dbq.Album.SortTitle
- artistRefFrom now populates SortName from dbq.Artist.SortName
- New artistRefFromCovered helper builds CoverURL from nullable cover_album_id
- New HomePayload view type for GET /api/home response body
2026-05-01 17:56:19 -04:00
bvandeusen 302a15a209 test(recommendation): fix mislabeled compile-time assert in home tests
Replaces var _ = func() pgtype.UUID { ... } with var _ map[pgtype.UUID]struct{}
which actually verifies map-key comparability, matching the comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:54:40 -04:00
bvandeusen d25b1f9291 feat(recommendation): add HomeData composite service for /api/home
Runs five SQL queries in parallel (recently added albums, most played
tracks, last played artists, rediscover albums, rediscover artists) via
goroutines with mutex-guarded first-error capture. Rediscover sections
merge primary + fallback results, de-duping by pgtype.UUID map key.
All HomePayload slices are non-nil so the API handler encodes [] not null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:06:43 -04:00
bvandeusen b31723c5e2 feat(db): add M6a library-list + artist-tracks queries
Appends ListArtistsAlphaWithCovers, ListAlbumsAlphaWithArtist,
CountAlbums, and ListArtistTracksForUser queries; regenerates sqlc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:03:10 -04:00
bvandeusen 5ed3c20b74 feat(db): add M6a rediscover queries (albums + artists, with fallbacks)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:00:20 -04:00
bvandeusen 10c84a536e feat(db): add M6a home-section queries (recently added, most played, last played)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 08:56:53 -04:00
bvandeusen fd77217b7c docs: add M6a home page implementation plan
20 tasks covering backend SQL queries, recommendation service,
three new API endpoints (/api/home, /api/library/albums,
/api/artists/{id}/tracks), and frontend rewrite of / as the home
page composing four horizontal-scroll sections, plus new
/library/artists and /library/albums wrapping-grid pages.
ArtistRow component retired in favor of circular ArtistCard
(takes the click-target bug with it). AlbumCard gets the
FabledSword polish pass with a play-button overlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:32:00 -04:00
bvandeusen fd053e76ca docs: add M6a home page redesign + library relocation spec
Captures the brainstorm for the first UI polish slice (Fable #349):
new `/` home page with 4 sections (Recently added / Rediscover /
Most played / Last played) as independent horizontal-scroll rows
with arrow buttons, library list relocated to `/library/artists`
and `/library/albums` as wrapping grids with alphabetical dividers,
ArtistCard (circular) replacing ArtistRow (retires the click-target
bug), AlbumCard polish pass with play-button overlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:07:37 -04:00
bvandeusen 2f3326aee6 feat(web): suggestion feed on /discover (search-empty default) 2026-05-01 06:51:41 -04:00
bvandeusen 7f18a04161 feat(web): DiscoverResultCard attribution prop for M5c suggestions 2026-05-01 06:25:42 -04:00
bvandeusen 95b706836d feat(web): API client for /api/discover/suggestions 2026-05-01 06:24:21 -04:00
bvandeusen ae2d69d378 feat(api): /api/discover/suggestions handler 2026-05-01 06:23:08 -04:00
bvandeusen 277898a49a feat(recommendation): SuggestArtists service for M5c
Add per-user artist-suggestion service ranking out-of-library MBIDs by
signal x similarity. Single-CTE SQL collects user likes (5x weight) and
recency-decayed plays, joins against artist_similarity_unmatched, and
filters in-library candidates plus non-terminal lidarr_requests. The
service resolves top-3 attribution seeds to artist names in a batched
GetArtistsByIDs call so the UI can render "because you liked X" reasons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 06:20:02 -04:00
bvandeusen 5e73f590a9 feat(similarity): persist unmatched similar-artist MBIDs for M5c
upsertArtistSimilar keeps the existing matched path (top-K rows into
artist_similarity) and adds a parallel unmatched-persist loop with the
same top-K cap. Empty-name rows are skipped — we can't render a
suggestion card without a name. Logs unmatched-side errors at WARN
without aborting the tick (mirrors the matched-path policy).
2026-05-01 06:07:22 -04:00
bvandeusen be23ae488d feat(listenbrainz): expose Name on SimilarArtist for M5c suggestions 2026-05-01 05:51:29 -04:00
bvandeusen 2ca09749d9 feat(db): add artist_similarity_unmatched schema (migration 0012) 2026-05-01 05:50:22 -04:00
bvandeusen cf1b75ca12 docs(plan): add M5c suggested-additions implementation plan
9 tasks: migration 0012 (artist_similarity_unmatched), ListenBrainz client
extension (SimilarArtist.Name), similarity worker extension to persist
unmatched MBIDs, recommendation.SuggestArtists service with single CTE,
GET /api/discover/suggestions handler, frontend client + types, DiscoverResultCard
attribution prop, SuggestionFeed component + /discover integration.
2026-04-30 23:02:41 -04:00
bvandeusen a0a9fb201b docs(spec): add M5c suggested-additions design
Personalized artist suggestions on /discover (search-input-empty state).
On-demand SQL ranks out-of-library MBIDs from artist_similarity_unmatched
(new table, populated by extending the M4b similarity worker) by per-user
signal: likes weighted 5x plus recency-decayed plays (exp(-age_days/30)).
Top-12 with top-3 contributing seeds attributed per card. Reuses the M5a
DiscoverResultCard + POST /api/requests artist-add path.
2026-04-30 22:48:55 -04:00
bvandeusen 3e3ad89645 fix: T16 verification cleanups
- golangci-lint: errcheck on resp.Body.Close in Lidarr client + revive
  unused-parameter in delete_test.go
- coverage: 2 error-branch tests added to lidarrquarantine.Service
  (DeleteFile + DeleteViaLidarr track-not-found paths) bring per-package
  coverage to 81.4% (target >=80%)
- search/tracks.test.ts: same TrackMenu name-collision fix T13 applied to
  TrackRow.test.ts — exact-string button match instead of regex
2026-04-30 20:49:11 -04:00
bvandeusen 3bfec944c7 feat(web): /admin/quarantine aggregated queue with resolution actions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:34 -04:00
bvandeusen 0d7a65cffb feat(web): /library/hidden user-facing quarantine view
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:35:11 -04:00
bvandeusen 88ff997af7 feat(web): mount TrackMenu in TrackRow + PlayerBar 2026-04-30 20:31:56 -04:00
bvandeusen 8da4a3f9c0 feat(web): TrackMenu overflow + FlagPopover for the quarantine flow 2026-04-30 20:29:07 -04:00
bvandeusen b653e83478 feat(web): API client modules for quarantine + admin quarantine 2026-04-30 20:26:07 -04:00
bvandeusen b7f8136502 fix(server): wire real Lidarr clientFn for requests + quarantine services
Both lidarrrequests.Approve and lidarrquarantine.DeleteViaLidarr need a
working client factory; the previous nil clientFn made them always return
ErrLidarrDisabled even when /admin/integrations had a valid Lidarr config
saved. Per-call factory re-reads config so admin saves take effect without
restart, matching the Service contract.
2026-04-30 20:23:07 -04:00
bvandeusen 6fedd495be feat(api): /api/quarantine user + admin endpoints + Service wiring
User-facing /api/quarantine handlers (POST flag, DELETE unflag, GET mine)
plus the five /api/admin/quarantine endpoints (queue, resolve, delete-file,
delete-via-lidarr, actions). Mount() and the handlers struct now accept the
lidarrquarantine.Service, constructed in server.go alongside the existing
lidarrrequests wiring.
2026-04-30 20:19:39 -04:00
bvandeusen ba81e4f2ac feat(api): route track-list reads through user-context quarantine filter 2026-04-30 20:09:04 -04:00
bvandeusen 559fb5dd2c feat(db): add user-context track query variants honoring quarantine 2026-04-30 19:49:00 -04:00
bvandeusen ef3fffb3ea fix(lidarrquarantine): keep LidarrAlbumMbid nil on Resolve/DeleteFile audit rows + cover idempotent Resolve
The audit log treats lidarr_album_mbid as a 'this row triggered Lidarr'
marker. Setting it on Resolve/DeleteFile (where Lidarr is never contacted)
muddied that semantic. Reverting to plan: only DeleteViaLidarr writes the
mbid. Adds an idempotency test for Resolve over a track with zero rows
(audit row written, affected_users=0, mbid nil).
2026-04-30 19:42:19 -04:00
bvandeusen 782e72b595 feat(lidarrquarantine): admin actions Resolve/DeleteFile/DeleteViaLidarr 2026-04-30 19:37:40 -04:00
bvandeusen 7a0bc1f815 fix(lidarrquarantine): translate FK violation to ErrTrackNotFound + drop unneeded *string AlbumTitle
- Flag now detects Postgres SQLSTATE 23503 (foreign_key_violation) on
  UpsertQuarantine and surfaces ErrTrackNotFound instead of a wrapped
  generic. T8's handler can now return 404 track_not_found cleanly.
  Constraint-name guard ('track' substring) keeps a future user_id FK from
  mis-mapping to the same error.
- AdminQueueRow.AlbumTitle is now string (not *string). albums.title is
  NOT NULL upstream; the empty-string-to-nil dance was answering a
  nullability question that doesn't exist in the schema.
- Add TestFlag_NonexistentTrackReturnsErrTrackNotFound covering the new
  branch.
2026-04-30 17:42:55 -04:00
bvandeusen b4fe224a67 feat(lidarrquarantine): Service Flag/Unflag/ListMine/ListAdminQueue 2026-04-30 17:36:27 -04:00
bvandeusen e6e3f297d6 fix(dbtest,library): include quarantine tables in ResetDB; clarify DeleteTrackFile docstring
- dbtest.ResetDB.dataTables now truncates lidarr_quarantine + lidarr_quarantine_actions
  alongside the other M2-M4 data tables. Without this, M5b service tests would
  inherit residual state across runs.
- DeleteTrackFile godoc spells out the post-file/pre-DB failure window
  reconciles via the next library scan; the function is retry-safe by design,
  not atomic.
2026-04-30 17:32:08 -04:00
bvandeusen d0523da520 feat(library): DeleteTrackFile (rm file + tracks row, album/artist preserved) 2026-04-30 17:04:21 -04:00