release v2026.05.13.0: SSE live updates + offline cache + per-item rendering #43

Merged
bvandeusen merged 262 commits from dev into main 2026-05-13 22:36:49 -04:00
bvandeusen commented 2026-05-13 22:29:31 -04:00 (Migrated from git.fabledsword.com)

Substantial batch since v2026.05.12.1 — 45 commits across SSE live updates, the
offline cache pass, cover caching, and the per-item rendering migration.

Live updates (#392, #402)

  • New SSE event bus at /api/events/stream — like/unlike, quarantine flag/unflag,
    playlist mutation, request status change, scan run lifecycle all publish to
    the bus
  • Flutter consumer with screen-scoped invalidation: track/album/artist likes,
    playlist edits, and quarantine changes propagate cross-device without manual
    refresh
  • Lidarr reconciler publishes request.status_changed on completion so the
    Requests screen updates when fulfillment lands

Per-user playlist scheduling (#403)

  • gocron-based scheduler refreshes each user's system playlists at 03:00 in
    their local timezone (CRON_TZ-prefixed cron expressions)
  • New PUT /api/me/timezone + flutter_timezone capture; Flutter sends device
    IANA tz on login/app-start/weekly. Web does the same.
  • Migration 0026 adds users.timezone + users.timezone_updated_at

Discover system playlist + diversity (#400)

  • Fixed: Discover playlist failed to build (SELECT DISTINCT + ORDER BY plan error)
  • Hardened bucket failure handling so one bad bucket doesn't kill the build
  • Daily seed rotation (userIDHash + day-of-year) + 0.1 jitter + 12/13 head/tail
    split so the same library produces noticeably different mixes day-to-day

Flutter UI polish

  • #393: always-visible play buttons on home carousel cards (hover doesn't work
    on touch)
  • #396: full-screen now-playing polish — gradient backdrop from dominant cover
    color, Hero animation from mini bar, AnimatedSwitcher crossfade on track
    change
  • #401: now-playing row highlight on TrackRow + PlaylistTrackRow (web + Flutter)

Cold-start version check

  • Non-blocking VersionGate with 1m active-use polling, optimistic render,
    bounded 3s/2s timeouts. The cold-start spinner-on-/healthz behavior is gone;
    the version banner surfaces when the server requires a newer client without
    blocking app render.

Offline cache completion (#357 follow-ups)

  • Drift-first homeProvider — last /api/home payload survives app restart so
    Home paints from disk on cold open
  • library_changes retention compactor: trims rows >30 days old daily
  • Drift-first Liked / History / MyQuarantine tabs — Library screen now reads
    cache-first across the board with SWR refresh on visit

Cover caching

  • cached_network_image swap: every cover URL now lives in a disk-persistent
    cache (path_provider temp dir, URL-keyed). Scroll-off + scroll-back paints
    from disk. App restart paints from disk.
  • Sync prewarm: SyncController fires cover downloads for newly upserted
    albums + playlists at concurrency=3 after each delta commit. Newly synced
    library is fully cached before the user scrolls.

Per-item rendering (the big shift)

  • New /api/home/index endpoint returns sections of entity IDs only (~10×
    smaller than /api/home). Old endpoint stays for web compat.
  • HydrationQueue (concurrency=4, in-flight dedup) drives per-entity REST
    fetches against /api/albums/:id, /api/artists/:id, /api/tracks/:id when
    drift is empty for a tile
  • Skeleton placeholders match real card dimensions — no layout shift when
    content lands
  • AnimatedSwitcher cross-fade on every tile (220ms easeOut) so content
    "settles into place" rather than hard-cutting from skeleton
  • Applied to: Home (full pilot), Liked tabs, Playlist detail (skeleton-row
    variant — see deferred note)
  • Cover CachedNetworkImage fadeInDuration bumped 0 → 120ms: imperceptible on
    cache hits, smooth on misses

Net cold-visit behavior: shaped page paints instantly with skeletons,
content cascades in as the hydration queue drains. Warm visits paint fully
from drift in the first frame.

Deferred

  • Full per-track playlist detail (Fable #405): the unavailable-entries data
    model (playlist rows whose underlying track was deleted) requires a schema
    change to support cleanly. Skeleton-row variant in this release captures
    ~80% of the perceptual win; revisit only if cold visits to very large
    playlists still feel sluggish.
  • CachedHomeSnapshot cleanup (Fable #406): legacy snapshot table + homeProvider
    left in place as revert path. Drop after this batch proves out on real
    devices.

Operator notes

  • Drift schema bumps 2 → 6 (4 new tables across the batch). Migrations are
    in-place; first launch on this build will run them.
  • New users.timezone column on the server (migration 0026)
  • In-app updater should upgrade cleanly from v2026.05.12.0
  • Pull the new server container image and roll prod
Substantial batch since v2026.05.12.1 — 45 commits across SSE live updates, the offline cache pass, cover caching, and the per-item rendering migration. ## Live updates (#392, #402) - New SSE event bus at /api/events/stream — like/unlike, quarantine flag/unflag, playlist mutation, request status change, scan run lifecycle all publish to the bus - Flutter consumer with screen-scoped invalidation: track/album/artist likes, playlist edits, and quarantine changes propagate cross-device without manual refresh - Lidarr reconciler publishes request.status_changed on completion so the Requests screen updates when fulfillment lands ## Per-user playlist scheduling (#403) - gocron-based scheduler refreshes each user's system playlists at 03:00 in their local timezone (CRON_TZ-prefixed cron expressions) - New PUT /api/me/timezone + flutter_timezone capture; Flutter sends device IANA tz on login/app-start/weekly. Web does the same. - Migration 0026 adds users.timezone + users.timezone_updated_at ## Discover system playlist + diversity (#400) - Fixed: Discover playlist failed to build (SELECT DISTINCT + ORDER BY plan error) - Hardened bucket failure handling so one bad bucket doesn't kill the build - Daily seed rotation (userIDHash + day-of-year) + 0.1 jitter + 12/13 head/tail split so the same library produces noticeably different mixes day-to-day ## Flutter UI polish - #393: always-visible play buttons on home carousel cards (hover doesn't work on touch) - #396: full-screen now-playing polish — gradient backdrop from dominant cover color, Hero animation from mini bar, AnimatedSwitcher crossfade on track change - #401: now-playing row highlight on TrackRow + PlaylistTrackRow (web + Flutter) ## Cold-start version check - Non-blocking VersionGate with 1m active-use polling, optimistic render, bounded 3s/2s timeouts. The cold-start spinner-on-/healthz behavior is gone; the version banner surfaces when the server requires a newer client without blocking app render. ## Offline cache completion (#357 follow-ups) - Drift-first homeProvider — last /api/home payload survives app restart so Home paints from disk on cold open - library_changes retention compactor: trims rows >30 days old daily - Drift-first Liked / History / MyQuarantine tabs — Library screen now reads cache-first across the board with SWR refresh on visit ## Cover caching - cached_network_image swap: every cover URL now lives in a disk-persistent cache (path_provider temp dir, URL-keyed). Scroll-off + scroll-back paints from disk. App restart paints from disk. - Sync prewarm: SyncController fires cover downloads for newly upserted albums + playlists at concurrency=3 after each delta commit. Newly synced library is fully cached before the user scrolls. ## Per-item rendering (the big shift) - New /api/home/index endpoint returns sections of entity IDs only (~10× smaller than /api/home). Old endpoint stays for web compat. - HydrationQueue (concurrency=4, in-flight dedup) drives per-entity REST fetches against /api/albums/:id, /api/artists/:id, /api/tracks/:id when drift is empty for a tile - Skeleton placeholders match real card dimensions — no layout shift when content lands - AnimatedSwitcher cross-fade on every tile (220ms easeOut) so content "settles into place" rather than hard-cutting from skeleton - Applied to: Home (full pilot), Liked tabs, Playlist detail (skeleton-row variant — see deferred note) - Cover CachedNetworkImage fadeInDuration bumped 0 → 120ms: imperceptible on cache hits, smooth on misses **Net cold-visit behavior:** shaped page paints instantly with skeletons, content cascades in as the hydration queue drains. Warm visits paint fully from drift in the first frame. ## Deferred - Full per-track playlist detail (Fable #405): the unavailable-entries data model (playlist rows whose underlying track was deleted) requires a schema change to support cleanly. Skeleton-row variant in this release captures ~80% of the perceptual win; revisit only if cold visits to very large playlists still feel sluggish. - CachedHomeSnapshot cleanup (Fable #406): legacy snapshot table + homeProvider left in place as revert path. Drop after this batch proves out on real devices. ## Operator notes - Drift schema bumps 2 → 6 (4 new tables across the batch). Migrations are in-place; first launch on this build will run them. - New users.timezone column on the server (migration 0026) - In-app updater should upgrade cleanly from v2026.05.12.0 - Pull the new server container image and roll prod
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#43