Commit Graph

784 Commits

Author SHA1 Message Date
bvandeusen 5613fec5ef fix(flutter): PlaylistsApi.list parses {owned, public} envelope
The server has always returned an enveloped response from GET
/api/playlists; the existing client decoded it as a flat List which
fails at runtime against the actual Map shape. Existing playlists
list screen would have been broken on any production instance.

list() now returns PlaylistsList { owned, public }. The existing
list screen consumes lists.all (owned + public flattened) — same
visible output as the previous flat-list assumption.

This unblocks the home parity slice which needs the same endpoint
for the new Playlists row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:35:34 -04:00
bvandeusen 28b07c7ef4 fix(flutter/android): permit cleartext to 127.0.0.1 for just_audio loopback proxy
Audio playback failed against the prod HTTPS server with
"Cleartext HTTP traffic to 127.0.0.1 not permitted". Diagnostic
logging confirmed _baseUrl was correctly set to the prod URL — the
127.0.0.1 wasn't coming from the server or our URL construction.

Root cause: just_audio + audio_service spin up a local HTTP proxy on
loopback to inject the Authorization header, because Android's
MediaSession API can't pass headers down to ExoPlayer directly.
ExoPlayer connects to http://127.0.0.1:<random-port>; the proxy adds
the Bearer header and forwards to the real upstream HTTPS URL.
Loopback traffic doesn't leave the device.

This is the documented just_audio happy path — see
https://pub.dev/packages/just_audio#a-note-on-android-cleartext-traffic
— not a workaround for a plugin bug.

Adds network_security_config.xml that scopes cleartext permission to
127.0.0.1 ONLY. The base config keeps cleartextTrafficPermitted=false
so any actual remote endpoint must still be HTTPS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:24:47 -04:00
bvandeusen 83df3773ae fix(flutter): cover URLs send Bearer auth + audio handler diag logging
Two issues from on-device testing against prod HTTPS:

1. ServerImage was resolving cover URLs correctly
   (https://minstrel.fabledsword.com/api/albums/.../cover) but the server
   returned 401 because Image.network doesn't carry the session token
   automatically the way the browser sends cookies. Forwards the stored
   session token as an Authorization: Bearer header. No-op when no token
   is present.

2. The "Cleartext HTTP traffic to 127.0.0.1" audio error reproduced even
   after the previous defensive check landed, which means the URL handed
   to ExoPlayer has a valid scheme+host (just the wrong host). The check
   only catches scheme-less URLs, so it didn't fire. Added debugPrint
   logging at configure() and setQueueFromTracks() time to show the
   actual baseUrl + per-track resolved URL on the next reproduction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:15:56 -04:00
bvandeusen 088e8f4bf8 test(web/admin): align Lidarr bootstrap tests with file conventions
Refactors the new describe block to use the existing setupFresh()
helper pattern (mirrors setup() but with empty profile/folder lists
so we can verify the test response is what populates the dropdowns),
the lidarrSection() scoping helper (the cover-providers and SMTP
panels also have "Save changes" buttons that match unscoped queries),
and the correct mockQuery({ data: ... }) shape used elsewhere in the
file. Drops the duplicate afterEach (the file already has a global
one at line 131).

No behavior change in the tests themselves — same three cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:11:35 -04:00
bvandeusen c47ec6093f test(web/admin): cover Test-then-Save flow on /admin/integrations
Three new cases:
- Save on a fresh form calls test first, fills auto-defaults from the
  response, then put-config with non-zero defaults
- Save aborts when test fails and surfaces the error code
- An operator-picked default value survives the test → save sequence
  (auto-default $effect only fills when current value is 0/'')

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:10:37 -04:00
bvandeusen bca8622640 feat(web/admin): Save runs Test first, dropdowns honor test response
Click sequence on a fresh Lidarr config: enter URL + API key → click
Save. The page now calls testLidarrConnection first, stashes the
returned profiles/folders into local state, lets the existing
auto-default $effects pick first-of-each, then sends the put-config
request with non-zero defaults. If the test fails, the save aborts
and the error surfaces in saveError.

The explicit Test connection button keeps working and additionally
populates the dropdowns from the same response so an operator can
inspect/adjust before saving.

Each dropdown reads from its test-state array when present, falling
back to the existing query data (which only works after the first
successful save). list_errors entries surface as inline notes under
their respective dropdowns.

Closes the chicken-and-egg where saving required defaults that could
only be fetched after saving.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:58 -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 22f03a5fe8 test(server/admin): cover new test endpoint response shape
- Updates the happy-path test stub to handle the three list endpoints
  Lidarr exposes (qualityprofile / metadataprofile / rootfolder) and
  asserts profiles + folders make it through to the response.
- Adds a partial-failure case where one list endpoint 5xxs; verifies
  ok=true, the failing list is omitted, and list_errors carries its
  bucket code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:07:47 -04:00
bvandeusen 0bc17a85fb feat(server/admin): test endpoint returns profiles + folders on success
Extends POST /api/admin/lidarr/test response to include quality_profiles,
metadata_profiles, root_folders, and an optional list_errors map when the
Lidarr connection succeeds. The three list fetches run in parallel after
the ping; any per-list failure goes into list_errors so the response can
still report ok=true with whatever data did come back. Failed-ping
response shape is unchanged.

This lets /admin/integrations populate its dropdowns on a single
round-trip during first-time setup, fixing the chicken-and-egg where
the dropdowns previously gated on cfg.Enabled (which can't be true
until the first save, which itself needs non-zero defaults).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:07:15 -04:00
bvandeusen 602ef3bfdf fix(flutter): cover URLs resolve against server baseUrl + audio stream URL guard
Cover-art Image.network calls were passing server-relative URLs
(/api/albums/<id>/cover) straight to NetworkImage, which interprets
"no scheme" as file:/// and crashes with "No host specified in URI".
Same root cause regardless of HTTPS or HTTP server.

ServerImage wraps Image.network with a Riverpod read of
serverUrlProvider and prefixes the configured base URL. Absolute URLs
(e.g. discover screen's Lidarr image_urls) pass through unchanged.

Three call sites updated: album_card, artist_card, playlists_list_screen.
discover_screen left as-is — its row.imageUrl is already absolute (Lidarr
returns full URLs from MusicBrainz / Spotify metadata) and it has a
meaningful errorBuilder that ServerImage doesn't expose.

Also adds a defensive check in audio_handler.setQueueFromTracks: if
the constructed stream URL ends up scheme-less, throw a StateError
naming baseUrl + track.streamUrl + track.id instead of letting it
fall through to ExoPlayer which surfaces a confusing "Cleartext HTTP
traffic to 127.0.0.1 not permitted" error (Android's URL parser
defaults a scheme-less URI to localhost). User reported this exact
confusing error against an HTTPS prod server; the better message
will pinpoint where the empty baseUrl comes from on next reproduction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:47:25 -04:00
bvandeusen 751c2878f7 fix(flutter/android): production manifest + AudioServiceActivity
Two related Android setup gaps caught when running on a clean Pixel 6
emulator:

1. MainActivity extended FlutterActivity, but the audio_service
   plugin requires AudioServiceActivity. AudioService.init() threw
   PlatformException("The Activity class declared in your
   AndroidManifest.xml is wrong"), and the partial-engine-reinit that
   followed cascaded into a flutter_cache_manager SQLite EXCLUSIVE
   lock crash. Both errors clear with the correct base class.

2. The main manifest was missing two production permissions:
   - POST_NOTIFICATIONS — Android 13+ runtime requirement for the
     media notification (now-playing tile in the system tray).
     audio_service auto-prompts at init() time once declared.
   - ACCESS_NETWORK_STATE — used by ConnectionErrorBanner +
     flutter_cache_manager for connectivity awareness.

Note for future contributors: src/main/AndroidManifest.xml IS the
production manifest. The src/debug and src/profile variants only add
dev-only entries (e.g. INTERNET re-declared in debug for hot-reload).
There is no separate "release" manifest in Flutter's convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:33:30 -04:00
bvandeusen c4cccea775 refactor(server): remove bootstrap admin path
The bootstrap-admin-from-env-vars flow was a holdover from before
self-registration could create the first admin. Now that handleRegister
+ CreateUserFirstAdminRace promotes the first user to register on an
empty users table (verified shipped in v2026.05.08.2 / #376), the
bootstrap path is just a second source of truth that confuses operators
(and leaves an "admin" account in the DB that nobody asked for).

Removes:
- internal/auth/bootstrap.go + its test
- The auth.Bootstrap call from cmd/minstrel/main.go
- AuthConfig + AdminBootstrapConfig structs from config
- MINSTREL_AUTH_ADMIN_USERNAME / _PASSWORD env reads + their test
- The auth: block from config.example.yaml
- Bootstrap-related comments in docker-compose.yml + README.md

The README quickstart now points operators at /register on first start
instead of "watch the logs for a one-time password."

Existing instances keep their bootstrap-admin row in the DB; operators
who want it gone can register a new admin via /register, promote them
in /admin/users, then delete the old bootstrap user (last-admin guard
will require the new admin to be promoted first). No migration needed.

Recovery story for forgotten admin passwords now hinges on Fable #321
(admin password reset CLI) — currently the only path back in if no
other admin exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:14:33 -04:00
bvandeusen d5c8d316c5 fix(flutter): clear analyze --fatal-infos errors from admin parity slice
CI caught six issues against the new admin slice:

- AsyncValue<T> in this Riverpod 3.3.1 codebase exposes `.value`
  (returns T?), not `.valueOrNull`. Switched the three admin readers
  to match the established convention (player_bar, queue_screen,
  now_playing_screen all use `.value`).
- home_screen.dart still has ctx.push calls in _albumsRow / _artistsRow
  (carousel tap handlers) — restored the go_router import that
  Task 2 over-eagerly removed.
- admin_user_edit_sheet.dart had a single-line `if (!await ...) return;`
  that violated curly_braces_in_flow_control_structures. Wrapped in
  braces.
- admin_quarantine_item.dart doc comment had `<top>` placeholders that
  the analyzer flagged as unintended HTML. Rephrased without angle
  brackets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:02:14 -04:00
bvandeusen 9f6ceb1731 feat(flutter/admin): users screen with users + invites sections
Single-scroll layout with two _SectionHeader-anchored blocks. Users
rows tap into AdminUserEditSheet. Invites row shows token (monospace)
with copy button, optional note, and a redeemed badge if applicable.

"Generate invite" opens a small dialog with an optional note field
(server hardcodes 24h TTL — admins can't customise expiry). On
success the new token is shown in a copy-once result dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:57:53 -04:00
bvandeusen afcaf56b9c feat(flutter/admin): user row + edit sheet (toggles, reset, delete)
AdminUserRow shows username + admin/auto-approve badges; tap opens
AdminUserEditSheet bottom sheet.

Edit sheet: SwitchListTile pair for is_admin and auto_approve_requests
(both surface server errors via SnackBar, including the last-admin
guard). Reset password takes a typed-input dialog (>=8 chars enforced
client-side; admin supplies the new password). Delete routes through
TypedConfirmSheet ("DELETE") and pops the sheet on success.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:57:10 -04:00
bvandeusen 7d207e56f5 feat(flutter/admin): users + invites mutations
Extends AdminUsersController (build-only since slice 2) with setAdmin,
setAutoApprove, delete, resetPassword. is_admin/auto_approve toggles
optimistically swap the row and roll back on server error (including
the last-admin guard 4xx). resetPassword is admin-supplied; no
auto-generation mode exists server-side.

Adds AdminInvitesController + adminInvitesProvider with create
(prepends new invite to the list, returns it for the copy-once
dialog) and revoke (optimistic remove + rollback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:56:34 -04:00
bvandeusen 3e905db906 feat(flutter/admin): quarantine screen + typed-confirm sheet
ExpansionTile per aggregated quarantine row — collapsed shows track,
artist · album, and "{N} reports: {top reason}". Expanded reveals the
nested per-user reports with their reasons + optional notes.

Three-dot menu offers Resolve / Delete file / Delete via Lidarr; both
deletes route through TypedConfirmSheet which requires the user to
type "DELETE" before the action button enables.

Adds AdminQuarantineController with optimistic-removal + rollback,
mirroring AdminRequestsController's pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:56:00 -04:00
bvandeusen bd1bcab12b feat(flutter/admin): requests screen with approve/reject
AdminRequestsScreen renders rows from adminRequestsProvider, joining
requester user_id → username via adminUsersProvider for display
(falls back to UUID prefix when the users list hasn't loaded yet).
Approve fires immediately; Reject confirms via dialog. Both are
optimistic with rollback in the controller.

Adds AdminUsersController build-only (the read side) so the row join
works; mutation methods land in Slice 3 alongside the Users screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:54:58 -04:00
bvandeusen 8bb192fe54 feat(flutter/admin): adminRequestsProvider with optimistic approve/reject
AsyncNotifier removes the row optimistically on approve/reject; rolls
back to the pre-mutation list on server error. Invalidates
adminCountsProvider so the landing badge stays accurate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:54:05 -04:00
bvandeusen 408eed1f3d feat(flutter/settings): admin card visible to admins
Settings gains an _AdminSection that renders an "Admin" tile (Shield
icon → /admin) only when the current profile has is_admin = true.
Non-admins see the section as SizedBox.shrink() so the const ListView
children stay const.

Provides a discoverable second entry to the admin landing alongside
the kebab overflow shortcut.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:53:35 -04:00
bvandeusen 6b5d12f3a1 feat(flutter/admin): landing screen with section cards + counts
ListView of three AdminSectionCard tiles (Requests / Quarantine /
Users) showing live counts from adminCountsProvider. Pull-to-refresh
re-fans-out to the three list endpoints. Admin/Quarantine/Users
sub-routes will land in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:53:03 -04:00
bvandeusen 372e7eca86 feat(flutter/admin): API providers + adminCountsProvider
Single landing-screen provider fans out to requests/quarantine/users
list endpoints in parallel and returns counts for the section-card
badges. Per-section AsyncNotifier controllers land alongside their
screens in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:52:12 -04:00
bvandeusen 79d00ba001 feat(flutter/api): admin endpoint clients
Four small Dio-backed API classes mirroring the server's actual
contract (verified against internal/api/admin_*.go):

- Requests/Quarantine return flat lists; Users/Invites are enveloped
  ({"users": [...]}, {"invites": [...]}) and unwrap here
- setAutoApprove sends {auto_approve: bool} — different from the
  response field name auto_approve_requests
- resetPassword takes admin-supplied {password: string}, returns 204
- Invite create takes optional {note: string}; expiry is server-fixed
  at 24h

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:52:00 -04:00
bvandeusen 2b595c40cd feat(flutter): admin DTOs (request, quarantine item, user, invite)
Hand-rolled to match the server's actual JSON shapes:
- AdminRequest carries user_id (UUID, not username); kind switches
  which title field is the display name
- AdminQuarantineItem aggregates reports per track with reason_counts
  and a nested reports[] list, mirroring adminQueueRowView
- AdminUser includes display_name + auto_approve_requests
- Invite uses invited_by UUID + note (server hardcodes 24h TTL)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:51:33 -04:00
bvandeusen dfc08650e7 feat(flutter): wire admin routes + isAdmin gate
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>
2026-05-08 21:50:53 -04:00
bvandeusen fc0350cb96 feat(flutter): adopt MainAppBarActions in Search, Discover, Playlists, Settings
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>
2026-05-08 21:50:29 -04:00
bvandeusen 5546787f78 feat(flutter): adopt MainAppBarActions in Home + Library
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>
2026-05-08 21:49:30 -04:00
bvandeusen 6564d37a2a feat(flutter): MainAppBarActions widget with admin-gated overflow
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>
2026-05-08 21:48:52 -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 3f2822dfc6 feat(flutter): discover screen — search Lidarr + create requests
- models/lidarr.dart (LidarrSearchResult + LidarrRequestKind)
- api/endpoints/discover.dart (search + createRequest)
- discover/discover_screen.dart with TextField, kind toggle (Artists/Albums),
  results list with in_library / requested pills
- /discover route + explore icon on home AppBar (5 actions now: Library,
  Playlists, Search, Discover, Settings)
2026-05-08 14:53:06 -04:00
bvandeusen dfbeed01a6 feat(flutter): settings screen + 4-icon home AppBar
- models/my_profile.dart (MyProfile + ListenBrainzStatus)
- api/endpoints/settings.dart (profile / password / listenbrainz CRUD)
- settings/settings_screen.dart with three sections:
  - Profile: display_name + email, Save profile
  - Password: current + new + confirm, validates >=8 + match
  - ListenBrainz: token paste + scrobble-enabled toggle
- /settings route + Settings icon on home AppBar
- Home AppBar now: Library + Playlists + Search + Settings (was just Search)
2026-05-08 14:51:04 -04:00
bvandeusen 1ad67dbe59 fix(flutter/test): tolerate Riverpod 3's ProviderException wrapper 2026-05-08 14:43:31 -04:00
bvandeusen da1cb7b590 feat(flutter): queue screen + skipToQueueItem support
- 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.
2026-05-08 14:42:10 -04:00
bvandeusen 66545bf8c3 feat(flutter): library screen with 5 tabs (Artists/Albums/History/Liked/Hidden)
Models:
- history_event.dart (HistoryEvent + HistoryPage envelope)
- quarantine_mine.dart (QuarantineMineRow for /api/quarantine/mine)
- Renamed Page<T> -> Paged<T> to avoid collision with Material's Page

Endpoints:
- library_lists.dart: listArtists / listAlbums (paged)
- me.dart: history + quarantineMine
- likes.dart: extended with listTracks/Albums/Artists (paged)

UI:
- library_screen.dart: TabBar over 5 tabs
- Artists tab: 3-col grid
- Albums tab: 2-col grid
- History tab: list with relative-time formatting (matches HistoryRow.svelte)
- Liked tab: 3 stacked sections (artists scroll-row, albums scroll-row, tracks list)
- Hidden tab: list of quarantined tracks with reason pill
- /library route + library_music icon on home AppBar

First page only for v1 (limit 50). Infinite scroll deferred.
2026-05-08 14:40:26 -04:00
bvandeusen 5541171e94 feat(flutter): playlists list + detail screens
- 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.
2026-05-08 14:33:53 -04:00
bvandeusen 147d6e280e feat(flutter): bump deps to latest + Search screen slice
Major version bumps (riverpod 2->3, go_router 14->17, just_audio 0.9->0.10,
flutter_secure_storage 9->10, google_fonts 6->8, flutter_lints 4->6).
package_info_plus held at 8.x due to win32 conflict with secure_storage 10.x.

Riverpod 3 breaks: AsyncValue.valueOrNull -> .value, StateProvider replaced
with Notifier subclass. just_audio 0.10: ConcatenatingAudioSource -> setAudioSources.

Search slice:
- models/page.dart (generic Page<T> envelope)
- models/search_response.dart (3-facet wrapper)
- api/endpoints/search.dart
- search/search_provider.dart (debounced 250ms)
- search/search_screen.dart (TextField + 3 horizontal/vertical sections)
- Wired /search route + search button on home AppBar
2026-05-08 14:30:01 -04:00
bvandeusen 1b3f2e254b docs(readme): use :latest in quickstart compose instead of pinned tag 2026-05-08 13:50:53 -04:00
bvandeusen 4f669c26e9 fix(coverart/test): set MinInterval in rate-limit-serialization tests 2026-05-08 13:21:45 -04:00
bvandeusen b5a138bb27 fix(server): repair recursive withUser, gofmt 2 files (golangci-lint) 2026-05-08 12:05:35 -04:00
bvandeusen 6a444334ea fix(server/api/test): drop unused 'context' imports A3 sed left behind 2026-05-08 11:29:36 -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 47c316e211 fix(web/test): restore named-mock svelte-query overrides B3 stripped (integrations + requests) 2026-05-08 11:26:19 -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 923b8286ee fix(server/api/test): retype callRadio user param to dbq.User for withUser helper 2026-05-08 11:05:25 -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 4ce7be4296 fix(server): repair go vet failures from A1+A2 cleanup (orphan imports + needle deref) 2026-05-08 10:46:43 -04:00
bvandeusen c81491164a refactor(server/api/test): withUser helper; migrate 51 context-value sites (A3) 2026-05-08 10:40:03 -04:00