feat(#392): publish quarantine + playlist mutation events

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>
This commit is contained in:
2026-05-12 21:28:05 -04:00
parent 3ffa5608d8
commit ee7f0cdb42
4 changed files with 59 additions and 0 deletions
+41
View File
@@ -47,6 +47,47 @@ func (h *handlers) publishLikeEvent(userID, entityID pgtype.UUID, entityType str
})
}
// publishQuarantineEvent broadcasts a quarantine action. User-side
// flag/unflag are scoped to the user; admin-side resolve / delete-file /
// delete-via-lidarr are broadcast (empty UserID) because a single admin
// action can affect every user who'd flagged that track — every client
// invalidates and the non-affected ones no-op when the query result
// matches their cache.
func (h *handlers) publishQuarantineEvent(kind string, userID, trackID pgtype.UUID, broadcast bool) {
if h.eventbus == nil {
return
}
scope := ""
if !broadcast {
scope = uuidToString(userID)
}
h.eventbus.Publish(eventbus.Event{
Kind: kind,
UserID: scope,
Data: map[string]any{
"track_id": uuidToString(trackID),
},
})
}
// publishPlaylistEvent broadcasts a playlist mutation to the owner so
// their other clients invalidate the affected playlist (and playlist list)
// providers. Public-playlist subscribers are out of scope here — they'd
// need a separate "playlists.public_updated" broadcast, deferred until
// the multi-user case is exercised.
func (h *handlers) publishPlaylistEvent(kind string, ownerID, playlistID pgtype.UUID) {
if h.eventbus == nil {
return
}
h.eventbus.Publish(eventbus.Event{
Kind: kind,
UserID: uuidToString(ownerID),
Data: map[string]any{
"playlist_id": uuidToString(playlistID),
},
})
}
// publishRequestStatusChanged broadcasts a Lidarr request status flip to
// the request's original requester so their /requests page reflects the
// new state without manual refresh. Admin actors (approve / reject) still