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.
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
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
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.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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)
playlist mutation, request status change, scan run lifecycle all publish to
the bus
playlist edits, and quarantine changes propagate cross-device without manual
refresh
Requests screen updates when fulfillment lands
Per-user playlist scheduling (#403)
their local timezone (CRON_TZ-prefixed cron expressions)
IANA tz on login/app-start/weekly. Web does the same.
Discover system playlist + diversity (#400)
split so the same library produces noticeably different mixes day-to-day
Flutter UI polish
on touch)
color, Hero animation from mini bar, AnimatedSwitcher crossfade on track
change
Cold-start version check
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)
Home paints from disk on cold open
cache-first across the board with SWR refresh on visit
Cover caching
cache (path_provider temp dir, URL-keyed). Scroll-off + scroll-back paints
from disk. App restart paints from disk.
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)
smaller than /api/home). Old endpoint stays for web compat.
fetches against /api/albums/:id, /api/artists/:id, /api/tracks/:id when
drift is empty for a tile
content lands
"settles into place" rather than hard-cutting from skeleton
variant — see deferred note)
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
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.
left in place as revert path. Drop after this batch proves out on real
devices.
Operator notes
in-place; first launch on this build will run them.