The daily 03:00 scheduler rebuild (and the manual refresh endpoint) replace
a user's system playlists + You-might-like rows but published no event, so a
client left open across the rebuild served yesterday's snapshot until a
manual reload — the stale-tab case behind #968. Add a user-scoped
playlist.system_rebuilt event (envelope {kind,user_id,data:{}}) from both the
scheduler (bus threaded into NewScheduler) and handleSystemPlaylistRefresh.
Clients consume it to invalidate home / system-playlist views and proactively
re-pull a stale active queue. Issue #968.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 3a — extends the producer set onto the SSE bus.
quarantine events (5 producer sites):
- quarantine.flagged (user-side flag): broadcast so the flagger's other
clients invalidate their Hidden tab and admins' clients invalidate
their queue.
- quarantine.unflagged (user-side unflag): scoped to the user.
- quarantine.resolved / .file_deleted / .deleted_via_lidarr (admin
actions): broadcast because a single admin action can affect every
user who'd flagged that track.
playlist events (6 producer sites):
- playlist.created / .updated / .deleted: owner-scoped.
- playlist.tracks_changed: emitted for AppendTracks / RemoveTrack /
Reorder. Owner-scoped. Single kind for all three mutations because
the client invalidation logic is identical (refetch the detail).
Public-playlist subscribers (other users viewing someone else's public
playlist) intentionally left out — needs a separate broadcast kind, not
exercised yet at single-household scale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice 2 of #392 — wires the first producers onto the bus that slice 1
built. After this commit, an SSE subscriber sees real events fire:
- track.liked / track.unliked when the user toggles the heart on a track
(handleLikeTrack / handleUnlikeTrack). Album + artist like events
intentionally deferred — they're symmetric trivial follow-ups but the
operator's primary like surface is tracks.
- request.status_changed when a Lidarr request is created, cancelled,
approved, or rejected. Auto-approve will fire twice (pending then
approved) in rapid succession, which is semantically correct; client
invalidation handles that fine.
Events are user-scoped via row.UserID so admin approve/reject route to
the requester, not the admin acting. Helpers live in events_publish.go
so the wire shape (kind names, payload keys) stays in one place — future
producers in slice 3 reuse the same pattern.
events_publish.go is no-op when h.eventbus is nil so tests that
construct handlers without a bus continue to pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>