Adds /admin, /admin/requests, /admin/quarantine, /admin/users routes to
the ShellRoute (so PlayerBar persists). Redirect closure refuses any
/admin path when user.isAdmin is false. Server-side RequireAdmin
middleware remains the actual authority — this is UX.
Screens land in subsequent commits; this intermediate state won't build
until task 8/9 lands the AdminLandingScreen and the missing siblings
are stubbed out by task 11/13/16.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Search keeps its conditional clear-text button and gains the shared
nav widget after it. Discover/Playlists/Settings get an actions row
they didn't have before. The kebab is now reachable from every
top-level screen, which is the prerequisite for the Admin entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HomeScreen drops its 5 ad-hoc IconButton actions in favour of the shared
widget. LibraryScreen gains an actions row it didn't have before. Both
now consistent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shared AppBar actions for top-level screens — three primary icons
(Home/Library/Search, current screen suppressed) plus a kebab containing
Playlists/Discover/Settings and (only when isAdmin) Admin. Replaces the
ad-hoc per-screen IconButton lists; centralises admin-gating logic in
one place so /admin entry can't accidentally leak to non-admins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- audio_handler: override skipToQueueItem(index) -> _player.seek(zero, index: i)
- player/queue_screen.dart: list of MediaItems with current track highlighted
(left accent border + Now Playing badge), tap-to-jump on non-current rows
- /queue route + queue_music icon on /now-playing AppBar
Reorder + remove deferred for v1.
- models/playlist.dart (Playlist, PlaylistTrack, PlaylistDetail)
- api/endpoints/playlists.dart (list with kind=user|system|all, get detail)
- playlists/playlists_provider.dart (Riverpod family providers)
- playlists/playlists_list_screen.dart (with system-variant pill)
- playlists/playlist_detail_screen.dart (header + Play button + rows)
- /playlists + /playlists/:id routes wired
- Home AppBar: queue_music icon next to Search
Tracks with track_id=null render greyed-out + struck-through (matches
web's PlaylistTrackRow behavior). Tap a row plays from that position;
top-level Play button plays the full playable subset from track 0.
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.
W-T3 (617477b) consolidated toast rendering into a single <ToastHost />
mounted in +layout.svelte, but page-level tests render components without
the layout, so screen.getByTestId('toast') and role=status queries broke.
Mount ToastHost in beforeEach (auto-cleaned by svelteTesting()) and reset
the store with clearToast() in afterEach so toasts can't leak between
tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add audit.WriteOrLog: a one-line wrapper around Write that logs at
Warn and swallows the error, matching the package contract that
audit failures must not break user-facing operations.
Migrate the 13 call sites across 7 files in internal/api/ from the
3-line "if err != nil { logger.Warn(...) }" shape to a single call.
audit.Write stays exported for tests + any future caller that
needs strict semantics.
Adds three tests: success (no log), failure-via-closed-pool (Warn
record with action+err keys), and nil-logger (no panic). Tests
skip when MINSTREL_TEST_DATABASE_URL is unset, matching the
existing harness convention.