M5–M7 sweep + offline cache + in-app updates + DRY pass #35

Merged
bvandeusen merged 262 commits from dev into main 2026-05-10 21:09:39 -04:00
bvandeusen commented 2026-05-10 20:34:50 -04:00 (Migrated from git.fabledsword.com)

PR brings dev (~555 commits, 2026-04-29 → 2026-05-10) into main for a real-device test release.

Major themes

M5 / M6 / M7 server + web (Apr 29 → May 8)

  • M5a Lidarr integration (typed client, request/quarantine flows, admin UI)
  • M5b/c recommendation service + nav restructure
  • M6a request-progress counters + scan admin
  • M7 frontend sweep: branding (#363/#377), queue UI (#363), history (#364), playlists (#352 slices 2-3), cards/MBID extraction (#382), TheAudioDB artist art, in-flight tallies UI
  • M7 user management (#376 — first-registered-user is admin, promote/demote, invites)
  • M7 search filter chips (#368), track-actions menu (#372), theme toggle (#362)

Flutter offline mode (#357, plans A/B/C)

  • Server: append-only library_changes log + cursor-based delta sync at /api/library/sync
  • Flutter: drift cache (cached_artists/albums/tracks/likes/playlists/playlist_tracks), SyncController, cacheFirst reactive helper, drift-first migration of 7 cached-entity providers
  • Audio cache: just_audio LockCachingAudioSource with tiered LRU eviction; CachedIndicator + Download buttons across track surfaces; Settings → Storage card
  • Wire-format fix in this batch: discovered the sync handler emitted PascalCase JSON but Flutter reads snake_case — entire metadata cache had been writing empty rows. Closed via library_sync_views.go view structs + pinned wire-key tests.

Flutter parity (#356)

  • Search, Discover (Lidarr search + create requests), Queue UI, Library 5-tab, Settings (Profile / Appearance / Storage / Password / ListenBrainz / Admin), Playlists list+detail, Track-actions sheet, Admin (quarantine + requests + users), Listening history
  • User-facing requests view + cancel (closes the cleanest remaining gap)
  • systemVariant column + drift schema bump (closes the v1 limitation in plan C)

Web mobile responsive (#358)

  • Hamburger nav + MobileNavDrawer (off-canvas, focus trap, esc close)
  • PlayerBar 2-row mobile reflow + PlayerOverflowMenu (shuffle/repeat/queue/volume)
  • RowActionsMenu generic primary+⋮ for all 4 admin tables
  • AlphabeticalGrid h3 dividers (a11y borrow), HorizontalScrollRow coarse-pointer arrows

Web DRY pass (#375)

  • Test-utils batch (mocks/likes, mocks/quarantine, mocks/appState, fixtures/track) — ~46 test files swept
  • playlists.ts migrated to api.* wrapper; api.del made generic
  • safeLocalStorage, playlistTrackToRef, runMutation, CardActionCluster helpers
  • Theme-color hex DRYed to tokens.json
  • Auth ↔ player import cycle fully inverted via sessionEnd signal

Web stream resilience (#369 + this batch)

  • preload="auto" on the audio element
  • attachStallRetry — auto-recovery from stalled / waiting / network errors with bounded retries
  • Hidden prefetch <audio> for the next track (gap-free transitions)
  • audioLoader seam — Tauri shell can later swap to a Rust-backed cache without touching the player UI
  • Auto-poll /requests + /admin/requests every 12s while ingests in-flight (#369 piece)

In-app update flow (#397)

  • Server /api/client/version + /api/client/apk endpoints (auth-gated, APK rate-limited per user 60s)
  • Flutter banner mounts in shell, polls every 24h, downloads via dio, fires Android PackageInstaller intent through MainActivity MethodChannel + FileProvider
  • CI sequencing: release.yml polls the Forgejo release page for the APK that flutter.yml attached, drops it into client/, Dockerfile bakes it into the image at /app/client/
  • Web MobileAppDownload component on Settings → Mobile app; renders nothing when no APK bundled

Heads-up for the test pass

  • First sync after upgrading: drift gets fully populated for the first time. Pre-this-PR images had a broken sync wire format; offline browse showed empty rows. After deploying this image and the Flutter app re-runs sync from cursor 0, the cache should populate correctly.
  • Flutter drift schema bumps from v1 → v2 for the systemVariant column; existing installs auto-migrate via m.addColumn + cursor reset.
  • In-app update flow is end-to-end testable for the first time with this release — see #397 closeout note in Fable for the full first-tag-release walkthrough.

Test plan

  • CI on the PR passes (Go + web + Flutter)
  • Tag release after merge — image build should bundle the matching APK
  • On-device walkthrough: offline browse, add-to-playlist sheet (no system playlists), stream resilience under network blip, requests view + cancel, mobile responsive at 375/414/768
  • In-app update: existing 0.1.0 client → see banner → install → upgrade

🤖 Generated with Claude Code

PR brings `dev` (~555 commits, 2026-04-29 → 2026-05-10) into `main` for a real-device test release. ## Major themes ### M5 / M6 / M7 server + web (Apr 29 → May 8) - M5a Lidarr integration (typed client, request/quarantine flows, admin UI) - M5b/c recommendation service + nav restructure - M6a request-progress counters + scan admin - M7 frontend sweep: branding (#363/#377), queue UI (#363), history (#364), playlists (#352 slices 2-3), cards/MBID extraction (#382), TheAudioDB artist art, in-flight tallies UI - M7 user management (#376 — first-registered-user is admin, promote/demote, invites) - M7 search filter chips (#368), track-actions menu (#372), theme toggle (#362) ### Flutter offline mode (#357, plans A/B/C) - Server: append-only `library_changes` log + cursor-based delta sync at `/api/library/sync` - Flutter: drift cache (`cached_artists`/`albums`/`tracks`/`likes`/`playlists`/`playlist_tracks`), SyncController, `cacheFirst` reactive helper, drift-first migration of 7 cached-entity providers - Audio cache: just_audio LockCachingAudioSource with tiered LRU eviction; CachedIndicator + Download buttons across track surfaces; Settings → Storage card - **Wire-format fix in this batch**: discovered the sync handler emitted PascalCase JSON but Flutter reads snake_case — entire metadata cache had been writing empty rows. Closed via `library_sync_views.go` view structs + pinned wire-key tests. ### Flutter parity (#356) - Search, Discover (Lidarr search + create requests), Queue UI, Library 5-tab, Settings (Profile / Appearance / Storage / Password / ListenBrainz / Admin), Playlists list+detail, Track-actions sheet, Admin (quarantine + requests + users), Listening history - User-facing requests view + cancel (closes the cleanest remaining gap) - systemVariant column + drift schema bump (closes the v1 limitation in plan C) ### Web mobile responsive (#358) - Hamburger nav + MobileNavDrawer (off-canvas, focus trap, esc close) - PlayerBar 2-row mobile reflow + PlayerOverflowMenu (shuffle/repeat/queue/volume) - RowActionsMenu generic primary+⋮ for all 4 admin tables - AlphabeticalGrid h3 dividers (a11y borrow), HorizontalScrollRow coarse-pointer arrows ### Web DRY pass (#375) - Test-utils batch (`mocks/likes`, `mocks/quarantine`, `mocks/appState`, `fixtures/track`) — ~46 test files swept - `playlists.ts` migrated to `api.*` wrapper; `api.del` made generic - `safeLocalStorage`, `playlistTrackToRef`, `runMutation`, `CardActionCluster` helpers - Theme-color hex DRYed to tokens.json - Auth ↔ player import cycle fully inverted via `sessionEnd` signal ### Web stream resilience (#369 + this batch) - `preload="auto"` on the audio element - `attachStallRetry` — auto-recovery from `stalled` / `waiting` / network errors with bounded retries - Hidden prefetch `<audio>` for the next track (gap-free transitions) - `audioLoader` seam — Tauri shell can later swap to a Rust-backed cache without touching the player UI - Auto-poll `/requests` + `/admin/requests` every 12s while ingests in-flight (#369 piece) ### In-app update flow (#397) - Server `/api/client/version` + `/api/client/apk` endpoints (auth-gated, APK rate-limited per user 60s) - Flutter banner mounts in shell, polls every 24h, downloads via dio, fires Android `PackageInstaller` intent through MainActivity MethodChannel + FileProvider - CI sequencing: `release.yml` polls the Forgejo release page for the APK that `flutter.yml` attached, drops it into `client/`, Dockerfile bakes it into the image at `/app/client/` - Web `MobileAppDownload` component on Settings → Mobile app; renders nothing when no APK bundled ## Heads-up for the test pass - **First sync after upgrading: drift gets fully populated for the first time.** Pre-this-PR images had a broken sync wire format; offline browse showed empty rows. After deploying this image and the Flutter app re-runs sync from cursor 0, the cache should populate correctly. - **Flutter drift schema bumps from v1 → v2** for the systemVariant column; existing installs auto-migrate via `m.addColumn` + cursor reset. - **In-app update flow is end-to-end testable for the first time** with this release — see #397 closeout note in Fable for the full first-tag-release walkthrough. ## Test plan - [ ] CI on the PR passes (Go + web + Flutter) - [ ] Tag release after merge — image build should bundle the matching APK - [ ] On-device walkthrough: offline browse, add-to-playlist sheet (no system playlists), stream resilience under network blip, requests view + cancel, mobile responsive at 375/414/768 - [ ] In-app update: existing 0.1.0 client → see banner → install → upgrade 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#35