Two bundled fixes: ### Web: PlayerBar test failures PlayerBar renders both compact (md:hidden) and desktop (hidden md:flex) blocks; jsdom doesn't apply CSS media queries so both DOM trees are present in tests. screen.getByRole/getByText found duplicates → 14 test failures. Added data-testid="player-bar-compact" + "player-bar-desktop"; tests scope queries via within(getByTestId(...)). Compact is the focus of #358, so most tests scope there. The "Up next" subgroup explicitly scopes to desktop since that copy was dropped from compact. ### Flutter: system playlists not loading cacheFirst was too conservative — when drift had user-created playlists from sync but no system playlists (For-You / Discover), fetchAndPopulate never fired (drift wasn't empty). Result: home tile row showed user playlists but never the system ones. Added cacheFirst alwaysRefresh option = stale-while-revalidate. playlistsListProvider opts in: yields cache immediately, then kicks off REST refresh in background. Drift watch() picks up the new rows and re-emits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Minstrel mobile client
Flutter (iOS + Android) sibling of the SvelteKit web SPA. v1 first slice ships: scaffold, auth, library browse (home / artist / album), likes, player with background audio + lock-screen controls.
Setup
cd flutter_client
flutter pub get
./tool/sync_shared.sh # copy tokens / error-copy / placeholders from ../web
dart run tool/gen_tokens.dart # regen lib/theme/tokens.dart from shared/fabledsword.tokens.json
flutter run
sync_shared.sh is idempotent. CI runs it on every build; rerun locally
whenever web/src/lib/styles/tokens.json,
web/src/lib/styles/error-copy.json, or
web/static/placeholders/album-fallback.svg changes.
Project layout
See docs/superpowers/specs/2026-05-02-flutter-mobile-foundation-design.md
section 3. Briefly:
lib/api/— dio client, ApiError, error-copy loader, per-surface endpoints.lib/auth/— server URL screen, login screen, AuthController + secure storage.lib/library/— home, artist detail, album detail, providers, widgets.lib/likes/— LikeButton + optimistic toggle controller.lib/player/— audio handler, player provider, mini PlayerBar, NowPlaying.lib/theme/— FabledSword token Dart class (generated), ThemeExtension, ThemeData.lib/shared/— routing (go_router shell), version gate, connection error banner.
shared/fabledsword.tokens.json and assets/error-copy.json are
synced from web/. Don't edit them directly — edit web/src/lib/styles/
and re-run ./tool/sync_shared.sh.
Architecture
- State: Riverpod 2 (
AsyncValue,AsyncNotifier, family providers). - HTTP: dio 5 with a Bearer-auth interceptor; 401 clears the session and the router redirects to login.
- Audio: just_audio wrapped by audio_service for background playback + lock-screen / notification controls.
- Auth: Bearer tokens in flutter_secure_storage. Server already
supports both cookie (web SPA) and Bearer (
internal/auth/session.go). - Routing: go_router with a ShellRoute so the PlayerBar persists
across navigation. Cold launch flow: no server-url →
/server-url, url set / no token →/login, token present →/home.
CI
.forgejo/workflows/flutter.yml runs on push to dev/main, tag pushes,
PR to main, and manual dispatch — path-filtered to flutter_client/**
plus the shared web inputs. Steps: sync, analyze, test, build APK.
Debug APKs upload as artifacts; release APKs attach to the Forgejo
release on tag.
Versioning
pubspec.yaml version mirrors the server tag (e.g. server v0.1.0
→ Flutter 0.1.0+1). The server's /healthz returns
min_client_version; old clients show an Update Required modal and
refuse to operate. Bump internal/server/version.go MinClientVersion
when a server-side change requires a paired client update.
Distribution
- Android: APK on the Forgejo release page. No Play Store for v1.
- iOS: TestFlight on tag (manual upload v1, automate later).
Out-of-scope for slice 1
Search, discover, requests, settings beyond server URL, admin, listening history, playlists, offline cache (#357). Those land in subsequent slices with their own brainstorm/spec/plan cycles. Per the operator's M7 ordering decision (2026-05-02), slice 2+ pauses to ship the missing web features first (#352 playlists, #362 theme toggle, #363 queue UI, #364 listening history, etc.) before mirroring on Flutter.