-
v2026.06.02
Stablereleased this
2026-06-02 22:30:20 -04:00 | 239 commits to main since this releaseSame-day re-cut on the per-day mutable CalVer scheme. This re-cut is a hotfix for the v2026.06.02 (run #289) connectivity regression — fresh installs of the prior cut couldn't sign in and were stuck at the Welcome screen.
Hotfix: Android interceptor order (PR #76)
NetworkModule.ktchainedBaseUrlInterceptorbeforeAuthCookieInterceptor. Both interceptors scope work onhost == PLACEHOLDER_HOST(drift #568/#569, to keep the session cookie off Coil image fetches to MusicBrainz / coverartarchive / Lidarr). With BaseUrl first, the URL was rewritten to the real Minstrel host before auth ran — so auth's placeholder check failed on every request, the session cookie was never attached on outgoing requests, andSet-Cookiefrom login was never captured. TheAuthCookieInterceptorTestchains the interceptors in the correct order, which is why the regression went undetected. Fix: swap to(auth, baseUrl, logging). Client-side only — server is unchanged.Anyone who installed the prior v2026.06.02 APK needs to re-install from this release.
Highlight: drift audit shipped (26 findings)
Multi-system audit run on 2026-06-02: 14 auditors (12 subsystem + 2 cross-cutting) fanned out across server / Android / web / Flutter looking for the 10-pattern drift taxonomy. 26 candidate findings surfaced; 24 confirmed by adversarial verify and shipped, 1 cancelled as duplicate, 1 doc-only correction. Scribe parent task: project 12 / task 552.
Highest-impact fixes
- Server
/api/meprofile shape (#578). The endpoint emitted UserView (id/username/is_admin) but Android's MyProfileWire expected display_name + email. Every Settings → Profile open silently saw nulls; saving from that blank state DESTROYED stored profile fields via the "empty string clears to NULL" path. Server now returns the full profile shape. Existing test extended; new regression-guard test added. - AuthCookieInterceptor scoping (#568 #569). The Minstrel session cookie was being attached to every external request that the shared OkHttpClient services (Coil image fetches to artwork.musicbrainz.org / coverartarchive.org / Lidarr /MediaCover URLs), and a 401 from any external host wiped the user's session. Both attach + clear now scoped to the placeholder.invalid sentinel host — mirrors the BaseUrlInterceptor fix from earlier today.
- System-playlist rotation alignment (#563 #564). Server's rotation whitelist had
for_you+discoveronly but migrations 0021 + 0028 added 6 more variants; plays from Rediscover / Deep Cuts / Songs Like X / etc. didn't advance the per-user rotation. Android also emittedsource="playlist:<variant>"while the server matched the bare variant, so system-mix plays from Android Home never registered at all. Whitelist extended; Android source format aligned with web's bare-variant contract. - Cold-boot resume coordination (#560 #562). ResumeController could race the MediaController IPC handshake — setQueue early-returned on null controller and the persisted queue was silently dropped. ResumeController now awaits a new
awaitReady()suspend before calling setQueue, and setQueue gains anautoplayopt-out so cold-boot resume restores paused (not auto-playing). - Cross-device unlike on Android (#570). LikesRepository.refreshIds only INSERTed — rows the server no longer surfaced (because the user unliked elsewhere) stayed in the local cache forever. Atomic
replaceAllForUserDAO transaction now wipes-then-inserts so the local set is exactly what the server reports. - CONFLATED playback-error channel (#561). The reporter's burst-coalescing path was dead code —
Channel.CONFLATEDsilently dropped every emission except the latest. A network blip that failed 5 tracks surfaced only the last; the admin inbox only got one POST. NowChannel.BUFFERED. - Periodic GC worker (#565 #566 #567 #574 #575). New
internal/gcpackage — one Worker, 5 idempotent sweeps on a 1h tick: close stale play_events (24h), close idle play_sessions (6h) / empty-starved (1h), expire scrobble_queue failed rows (14 days), reset stuck system_playlist_runs (10 min), delete expired password_resets. Closes the lifecycle-gap cluster from the audit in one shot. - Profile + lifecycle UX (#553 #554 #555 #557 #558 #559 #571 #572 #576 #577). Tailwind class fix on the playback-errors Delete confirm button; play_started
sourcefield added to the EventRequest TS type; coverage rollup test extended to deezer/lastfm; /forgot-password + /reset-password/[token] reachable without auth; SSR-only redirects converted to client-side; scanner indexes .opus/.aac/.wav; delete.go docstring honest about missing reconcile; LikesRepository uses real user UUID + clears on user-switch; RequestsViewModel doesn't refresh() on offline-queued cancel.
Verify on device
- Fresh APK install: sign in succeeds and lands on Home (was stuck at Welcome).
- Android Settings → Profile shows stored display_name + email (was wiping them).
- Drag-dismiss + cold-boot of NowPlaying: pop animation latches, resume opens paused (not playing).
- /admin/playback-errors → Delete file modal: confirm button is the proper Oxblood destructive color.
- Tap Cancel on a request while offline — row stays gone, no Error screen.
- Discover view shows artist artwork (regression for prior fix).
- New scrubber thumb reads as a small flat circle on a thin 4dp track.
Carried from prior cuts
Earlier same-day cuts already included: playback-errors slice (server + Android + admin UI), drag-dismiss latch, portrait lock, Material LibraryMusic icon (later reverted to Lucide.LibraryBig), notification tap → NowPlaying, NowPlaying polish (dominant swatch, slim scrubber, swipe tabs, refresh flicker fix, Start Radio preserves playback), web alphabet rail page-chasing, Songs Like fix, two-pane desktop NowPlaying, scrubber smoothing, CalVer-with-iteration APK versionName.
Downloads
- Server