Commit Graph

190 Commits

Author SHA1 Message Date
bvandeusen 65057473c6 refactor(web): break auth ↔ player import cycle via session-end signal (#375)
auth/store.svelte.ts no longer imports from $lib/player. Logout
broadcasts via auth/sessionEnd.svelte.ts (a tiny tick + outgoing
userId pair); player subscribes through $effect.root and owns the
player-specific teardown (clearPersistedQueue + playQueue([]) +
closeQueueDrawer()).

Cycle fully inverted — auth is now a pure leaf for player; future
session-end consumers (download cache, recent-search history) plug
in by adding their own effect on the same signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:29:30 -04:00
bvandeusen 084c202654 fix(web/test): MobileNavDrawer inert + integrations Save mocks Test first
MobileNavDrawer: Svelte 5 + jsdom binds `inert` as a property, not always
an attribute. Mirror the QueueDrawer.test.ts pattern that accepts either.

Integrations Save tests: Per commit bca8622 (Save runs Test first), the
two existing Save tests need to mock testLidarrConnection before clicking
Save — otherwise the Test step returns undefined, fails the ok check,
and putLidarrConfig is never reached. The newer 'Lidarr first-time
setup' suite already does this; just bringing the older two tests in
line. Also wrapped the assertions in waitFor since the put-config call
now resolves async after the test promise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 21:21:04 -04:00
bvandeusen 3ffd9beca0 fix(web): preserve per-row aria-labels through RowActionsMenu + test fixes
CI vitest run on 3f8a2c5 surfaced 17 failures across 5 test files; this
commit addresses 15 that are this batch's responsibility.

1. RowActionsMenu now accepts ariaLabel on RowAction. Defaults to label.
   Admin pages (requests/quarantine/users) pass per-row aria-labels
   matching the pre-batch buttons ("Approve Geogaddi", "Resolve Roygbiv",
   "Make alice admin", etc.) so screen readers + tests find them.

2. PlayerBar.test.ts — anchored regex /^(play|pause)$/i so the new
   "Player options" overflow ⋮ doesn't also match /play|pause/i.

3. MobileNavDrawer.test.ts — added vi.mock for $app/state, $app/navigation,
   and $lib/auth/store.svelte (mirrors Shell.test.ts pattern). Without
   these, SvelteKit's notifiable_store helper isn't bootstrapped in
   vitest and the suite fails to load.

The 2 remaining vitest failures are in /admin/integrations Save flow
(putLidarrConfig spy not called). Untouched by this batch and the
recent "Save runs Test first" refactor (bca8622) appears related —
flagging for operator verification, not chasing as a regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 21:16:48 -04:00
bvandeusen 3f8a2c511a fix(web): MobileNavDrawer focus uses querySelector, not conditional bind:this
bind:this requires an Identifier or MemberExpression — a ternary
like {i === 0 ? firstNavLink : undefined} is invalid. Replaced
with a single bind on the <aside> root, then querySelector('nav a')
to locate the first nav link at focus time. Same behaviour, valid
Svelte.

Caught by CI svelte-check on 268e12a (failed before the @const
fix landed in 1536860):
  src/lib/components/MobileNavDrawer.svelte:91:13
  https://svelte.dev/e/bind_invalid_expression

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:26:44 -04:00
bvandeusen 268e12a5eb feat(web): Modal overlay gets p-4 so max-w-md modals fit on phone
Without padding, max-w-md (448px) modals touch the viewport edges
on a 375px screen. p-4 on the overlay clamps them inside the safe
area. Covers all routes that use the shared <Modal> component
(discover track-confirm, quarantine typed-DELETE, users
password-reset, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:21:52 -04:00
bvandeusen 16e0e98943 feat(web): mobile mechanical sweep — pointer-aware arrows, h3 dividers, modal safe-area
- HorizontalScrollRow arrows bump 32px → 40px on coarse pointers
  (touch screens) for finger-friendly hits; mouse pointers unchanged.
- AlphabeticalGrid divider letter is now <h3> so screen readers
  announce it as a section heading. Margin reset to preserve layout.
- Modal overlay gets p-4 so max-w-md modals stay inside the safe
  area at 375px viewport. Covers all routes that use the shared
  <Modal> component (discover track-confirm, quarantine
  typed-DELETE, users password-reset, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:21:38 -04:00
bvandeusen e0cf527304 feat(web): RowActionsMenu generic primary + ⋮ for admin tables
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:19:09 -04:00
bvandeusen ed6d0936ca feat(web): PlayerBar reflows to two-row layout below md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:18:32 -04:00
bvandeusen d5983433c5 feat(web): PlayerOverflowMenu — shuffle/repeat/queue/volume in a menu
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:18:10 -04:00
bvandeusen df4cb434b2 feat(web): hamburger trigger + mobile drawer mount in Shell
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:17:35 -04:00
bvandeusen 857a8621ec feat(web): MobileNavDrawer off-canvas nav for <md viewport
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:17:17 -04:00
bvandeusen 7f395024a0 feat(web): mobileNav store for off-canvas drawer state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:16:38 -04:00
bvandeusen abb384f479 feat(web/admin): LidarrTestResult success branch carries profile + folder lists
Mirrors the server-side extension of POST /api/admin/lidarr/test. The
integrations page consumes these in the next commit to pre-fill the
quality/metadata/root-folder dropdowns on first-time Lidarr setup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:02 -04:00
bvandeusen c5dc3bd256 fix(web): exempt /register from auth guard so bootstrap admin can self-register
The +layout.svelte auth guard only treated /login as public, so any
unauthenticated visit to /register bounced to /login?returnTo=%2Fregister.
The "Register" link on the login page therefore looped back to itself,
making the first-user-becomes-admin bootstrap path (handleRegister +
CreateUserFirstAdminRace) unreachable in production.

Extracted isPublicRoute() into web/src/lib/auth/publicRoutes.ts so the
public-route set has one source of truth and is unit-testable. Test file
explicitly comment-tags /register as a #376 regression guard.

Closes the last gap in user-mgmt umbrella #376.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:59:43 -04:00
bvandeusen 3e263657c6 refactor(web): hoist .play-overlay CSS to app.css; remove triplicated <style> blocks (C2) 2026-05-08 11:28:58 -04:00
bvandeusen efef9934d4 refactor(web): migrate 4 open-coded cover URL sites to coverUrl() helper (C1 small-win) 2026-05-08 11:26:50 -04:00
bvandeusen 5a174c9ef0 refactor(web): converge PlaylistTrackRow drag-reorder onto @neodrag/svelte (B4) 2026-05-08 11:22:23 -04:00
bvandeusen 3e34f1f7b3 refactor(web/test): default svelte-query mock in vitest.setup; remove from 34 files (B3) 2026-05-08 11:08:41 -04:00
bvandeusen 909b36d5ad refactor(web/auth): extract user state to break auth<->player cycle (B2) 2026-05-08 10:49:39 -04:00
bvandeusen 68af48bb37 refactor(web/theme): read meta-theme hex from tokens.json (B1) 2026-05-08 10:48:03 -04:00
bvandeusen b1ef3c3688 feat(web/flutter): expand error-copy map with auth + validation + 404 codes 2026-05-08 07:58:54 -04:00
bvandeusen 6d16aa2de4 refactor(web/api): offsetGetNextPageParam helper; paging sites migrated (W4)
Final task of PR2 DRY pass. Extracts the verbatim
`offset + items.length >= total` math repeated across 8
createInfiniteQuery sites into a single Page<T>-shape helper.

The task spec described a bare-array helper signature
(lastPage: T[], pageSize-based stop), but no call site in this
repo uses that shape — every paged endpoint returns a Page<T>
envelope. The helper is named pageGetNextPageParam and matches
the actual canonical shape so the 8 copies could collapse.

Migrated:
- likes.ts: 3 sites (TrackRef, AlbumRef, ArtistRef)
- albums.ts: 1 site (AlbumRef)
- queries.ts: 4 sites (artists + 3 search facets)

history.ts left alone — uses has_more/total, different shape.
2026-05-08 07:33:07 -04:00
bvandeusen 617477b702 refactor(web): pushToast store + ToastHost; 8 sites migrated (W3) 2026-05-08 05:45:34 -04:00
bvandeusen 970752a153 refactor(web): Modal component; admin + discover modals migrated (W2) 2026-05-08 05:37:58 -04:00
bvandeusen 9c91a342e2 refactor(web/api): errCode + errMessage helpers; 41 sites migrated (W1) 2026-05-07 22:17:45 -04:00
bvandeusen 081b8e62d5 fix(web): a11y + reactivity warnings (paying down accumulated warnings)
Cleans up the 8 svelte-check warnings I deferred in prior CI runs.
Two distinct categories — handling them differently:

REAL REACTIVITY BUGS — fixed:
- AlbumMenu.svelte / ArtistMenu.svelte: cachedTracks initialized
  from the `tracks` prop only at component creation. When the menu
  is reused across rows (operator opens menu on one album, then
  another), the cache from the first open serves stale tracks.
  Fix: initialize cachedTracks to null; ensureTracks() reads the
  current `tracks` prop on each call before falling through to
  the network fetch.

INTENTIONAL PATTERNS LINT FLAGGED — suppressed with explanatory
comments:
- AlbumMenu / ArtistMenu / PlaylistCard menu containers: the
  onclick={(e) => e.stopPropagation()} is a guard against the
  window-level "any click closes the menu" listener — not
  user-facing interaction. A paired keyboard handler is
  intentionally absent (Escape-to-close lives on svelte:window
  onkeydown). Suppressed a11y_click_events_have_key_events with
  explanatory comments.
- CompactTrackCard overlay div: catches clicks on its inner
  LikeButton + queue button so they don't bubble to the wrapping
  play-card button. Inner buttons carry their own keyboard
  handling. Suppressed both a11y_click_events_have_key_events and
  a11y_no_static_element_interactions.
- theme.svelte.ts module-scope `_resolved = $state(resolve(_theme))`:
  module-init reads _theme to seed _resolved; subsequent updates
  go through setTheme() and the matchMedia listener. $derived
  doesn't apply at module scope. Suppressed state_referenced_locally
  with an explanatory comment.

Promised in the prior fix-forward: "I'll fix bugs when I see them
or open a follow-up task with a specific name, not park them in an
umbrella." Following through.
2026-05-07 18:38:44 -04:00
bvandeusen 72f46a885b feat(web/m7-user-mgmt): /settings expansion + /forgot-password (U3-T5a)
Lands four of the U3 frontend surfaces. Splits T5 because the
dispatch covering everything in one shot crashed the runtime
mid-execution; this is the salvageable half.

- /settings gains three cards alongside Appearance: Profile
  (display name + email), Password (current + new + confirm with
  client-side mismatch check), API Token (display + copy +
  regenerate-with-double-click-confirm). Server error codes map
  to clear toasts.
- /forgot-password — public; takes an email and always shows the
  success message regardless of whether the email is on file
  (mirrors the server's no-enumeration posture).
- Login page gets a "Forgot password?" link below the existing
  register link.
- API client functions for the four /me endpoints (changePassword,
  updateProfile, getAPIToken, regenerateAPIToken), the SMTP admin
  trio (getSMTPConfig, updateSMTPConfig, testSMTPConfig), and the
  forgot/reset auth pair (forgotPassword, resetPassword).

Tests for these surfaces + /reset-password page + admin SMTP card
land in T5b (next follow-up).
2026-05-07 17:33:48 -04:00
bvandeusen 777e32ca24 feat(web/m7-user-mgmt): /admin/users CRUD actions (U2)
Extends the U1 /admin/users page with the four U2 admin endpoints.

- "New user" button opens a modal: username, optional display
  name, password + confirm, optional admin checkbox. Validates
  password match client-side; surface server-side errors
  (username_taken, username_invalid, password_too_short).

- Per-row "Delete" opens a confirm modal explaining the cascade
  (plays, likes, sessions). Last-admin guard surfaces as a clear
  toast if the server refuses.

- Per-row "Reset password" opens a small modal: new password +
  confirm. Toast confirms success.

- Per-row "Enable / Disable auto-approve" toggles the
  per-user flag (the #355 sub-feature surface). Inline button
  state reflects the current value.

AdminUser type extended with auto_approve_requests; the badge
appears next to the admin badge when enabled.

Tests cover create-user submit, delete confirm flow, last-admin
toast on delete, reset-password submit, auto-approve toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 12:21:23 -04:00
bvandeusen 97d4dce7ee feat(web/m7-user-mgmt): /admin/users page + AdminTabs entry
New admin route /admin/users with two sections:

- Accounts — table of all users (username, optional display name,
  admin badge, created_at). Per-row "Make admin" / "Remove admin"
  button calls PUT /api/admin/users/{id}/admin. Last-admin guard
  surfaces as a toast ("Can't remove the last admin — promote
  someone else first").

- Invites — list of active invites with Copy + Revoke per row.
  "Generate invite" button creates a 24h token. Operator shares
  the token with the invitee, who enters it on /register.

AdminTabs gains a "Users" tab (5 tabs total). New typed client
functions in admin.ts (listUsers, updateUserAdmin, listInvites,
createInvite, deleteInvite) plus query factory functions
createAdminUsersQuery / createAdminInvitesQuery and query keys
qk.adminUsers / qk.adminInvites.

Tests cover: list rendering, admin badge, promote/demote actions,
last-admin guard toast, invite generation, revoke flow, empty states.
AdminTabs.test.ts updated to assert 5 tabs and Users active state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 12:06:08 -04:00
bvandeusen 6b23532a61 feat(web/m7-user-mgmt): /register page + login link
Public /register form: username, optional display name, password +
confirm, optional invite token. Submits to POST /api/auth/register
via the new register() helper in auth/store.svelte.ts. On success
the server sets the session cookie and the frontend redirects to /;
on error the page shows a code-specific message (invite_required,
invite_invalid, username_taken, etc.).

Login page gets a "Don't have an account? Register" link below the
form for symmetry.

Tests cover form rendering, password-mismatch client-side rejection,
the happy-path submit + redirect, and the invite-invalid error
mapping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 12:04:06 -04:00
bvandeusen e48d84cde1 feat(web/playlists): For-You tile play button refreshes and plays
Click on the For-You tile's play button now triggers a synchronous
refresh, then enqueues + auto-plays the freshly-built playlist.
Two-click-target tile pattern: tile BODY click still navigates to
the playlist detail page (existing behavior); the play button is
the new generate-and-play action.

Behavior is gated on playlist.system_variant === 'for_you' — every
other playlist (user, Discover, Songs-like-X) keeps the existing
"fetch detail, enqueue, play" flow. The atomic-replace BuildSystem-
Playlists creates a new playlist_id, so the play handler uses the
refresh response's id for the follow-up getPlaylist call rather
than the stale tile prop's id.

Empty-library response (playlist_id: null) is a no-op — clicking
play in a degenerate-empty library doesn't error, it just doesn't
start playback. Caches are invalidated post-refresh so the home
view re-fetches and the tile re-renders with the new id, avoiding
the corner case of a click-then-tile-body 404.

Tests cover the refresh-and-play happy path, the empty-library
no-op, and that non-For-You playlists keep the existing flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 10:40:23 -04:00
bvandeusen b20738f925 feat(web/playlists): Discover refresh button (detail + home kebab)
Operator can refresh their Discover playlist without waiting for
the next daily cron tick. Two surfaces, both gated on
system_variant === 'discover':

- Detail page header: "Refresh" button next to the cover art / meta.
- Home Playlists row tile kebab: "Refresh Discover" menu item.

Both call POST /api/playlists/system/discover/refresh, show a
confirmation toast, and invalidate the playlist + playlists queries
so the new content lands without a manual page reload.

Extends Playlist.system_variant union with 'discover'. Adds
refreshDiscover() typed client. Tests cover client behaviour
(success + empty-library) plus conditional rendering on both
surfaces.
2026-05-07 08:44:33 -04:00
bvandeusen c53bcb6c99 feat(server/web/coverart): manual Re-search missing art button
POST /api/admin/cover-sources/research bumps cover_art_sources_meta.
current_version unconditionally; the next enrichment pass re-eligibles
every 'none' row. Existing positively-sourced rows are not affected —
the version-mismatch eligibility rule only kicks in for 'none'.

Admin Cover Art panel gains a "Re-search missing art" button that
calls the endpoint and shows a confirmation toast. Operator's escape
hatch when:

- They've added a Last.fm key and want to retry failed rows
  immediately rather than waiting for the next scheduled scan.
- An upstream provider's catalog has updated (e.g. Deezer added a
  back-catalog import).
- They want to verify a fix end-to-end before the next scheduled
  scan fires.

SettingsService.BumpVersion (the unconditional version of T5's
BumpVersionIfProvidersChanged) is the single-call DB writer; both
endpoints route through it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 07:59:40 -04:00
bvandeusen d57004b085 feat(web/admin): sticky tabs at top of admin scroll container
The admin tab strip (Overview / Integrations / Requests / Quarantine)
scrolled out of view as the operator paged through long sections like
Library Scan, Cover Art, etc. Pin it to the top of the <main>
scroll container with sticky positioning, an opaque background to
hide content scrolling behind it, and z-10 to layer above section
content.
2026-05-07 06:53:06 -04:00
bvandeusen 0cc3d6255d feat(web/m7-recurring-scans): scan-schedule API client + query
Adds ScanSchedule type, getScanSchedule / updateScanSchedule
against the new admin endpoints, createScanScheduleQuery (60s
staleTime; refetches on focus + after PATCH invalidation — no
polling).

New qk.scanSchedule() key follows the existing tuple pattern.
ScanScheduleUpdate uses optional fields so mode=off updates can
omit per-mode fields entirely (server normalizes anyway).

Vitest covers the GET path, PATCH with body, and the mode=off
minimal-body case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 22:28:22 -04:00
bvandeusen 3b2d1009cd feat(web/m7-scan-progress): StageBadge + 4-card layout updates
Each of the 4 stage cards in the admin Library Scan section gains a
StageBadge in its header. The badge derives state via stageState()
from the existing ScanStatus payload — no new query, no schema
change. Active stage shows spinner + "Processing" in moss green;
done shows check + "Done" in moss green; pending and skipped show
muted text.

Card body now shows either the (possibly partial) numbers OR a
"Waiting for previous stage…" placeholder when the badge is
pending. The previous bottom-of-card "Pending…" / "Skipped."
fallback text disappears (the badge replaces it).

StageBadge.test.ts covers the four state renderings.
admin.test.ts adds one new case asserting the Processing badge
shows when scan.library is populated and in_flight: true. Existing
admin.test.ts assertions still pass (the default mock data has no
stage tallies + in_flight: false → all stages skipped, no Loader/
Check icons rendered).
2026-05-06 21:19:53 -04:00
bvandeusen 39a07a280c feat(web/m7-cover-sources): cover-providers API client + query
Adds CoverProvider type + capability union, getCoverProviders /
updateCoverProvider / testCoverProvider against the three new admin
endpoints, plus createCoverProvidersQuery (60s staleTime; refetches
on focus + after mutations rather than polling — settings rarely
change in operator time).

New api.patch helper added to client.ts (the existing helpers were
get/post/put/del; PATCH was missing).

New qk.coverProviders() key follows the existing tuple pattern.
api_key_set: bool replaces echoing the credential. UpdateCoverProviderPatch
uses optional fields so the operator can change just enabled, just
key, or both atomically.

Vitest covers GET path, PATCH path with body, version_bumped
response, POST /test path with empty body, ok=false error response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 13:11:08 -04:00
bvandeusen 00cd28e79b feat(server,web/m7-cover-sources): scan-orchestrator 4th stage
Adds an artist-art-enrich stage after cover-enrich in RunScan. The
stage drains EnrichArtistBatch with a configurable cap and the
operator's data_dir, persists processed/succeeded/failed tallies to
the scan_runs.artist_art_enrich jsonb column added in migration
0018.

The admin /admin Library Scan card grid expands from 3 to 4 columns
with the new "Artist art" card mirroring the cover-enrichment one
(Processed / Succeeded / Failed). The TS ScanStatus type gains an
optional artist_art_enrich field. The Go scanStatusResp gains the
matching ArtistArtEnrich json.RawMessage field.

cmd/minstrel/main.go (and admin scan-trigger handler) wire the new
RunScanConfig fields: ArtistEnrichCap reuses cfg.Library.CoverArtBackfillCap
for now (can be split if separate budgets are useful), DataDir is
cfg.Storage.DataDir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 13:03:36 -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 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
bvandeusen 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
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
bvandeusen 26f4c7a79f fix(web/m7-352): use typographic apostrophe in failed-variant copy 2026-05-04 19:16:25 -04:00
bvandeusen 412a1c00ac fix(web/m7-352): update listPlaylists test URL after kind filter addition 2026-05-04 18:52:40 -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 db361c8305 feat(web/m7-377): CompactTrackCard gains like + queue + kebab overlays 2026-05-04 17:49:19 -04:00
bvandeusen 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