Files
minstrel/internal/notifications/kinds.go
T
bvandeusenandClaude Opus 5.5 3a16732883
release / govulncheck (push) Successful in 15s
release / web (push) Failing after 17s
release / go (push) Successful in 1m13s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m9s
release / Build signed APK (releases and dev) (push) Successful in 5m19s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
feat: Minstrel no longer talks to Lidarr: its connection, Delete via Lidarr and the unmonitor options are gone, and the track reports are plain quarantine (M502 #5525)
Migration 0091 drops lidarr_config and renames the quarantine tables, types,
indexes and the history column off the Lidarr name. The Integrations Lidarr
card, the Reports "Delete via Lidarr" action, the merge and remove-track
unmonitor options, the Lidarr audit actions and error codes go, on the
server, web and Android. The README describes acquisition as it is now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:48:20 -04:00

142 lines
5.4 KiB
Go

// Package notifications is the one writer of the per-user notifications inbox
// (M489, #726). Producers across the server call Notifier.Notify; nothing else
// inserts into user_notifications.
//
// The event bus alone was fire-and-forget: a client that was not connected
// when a request completed, or when tracks went missing, never heard of it. A
// row here is the durable record. The bus only nudges open clients to come
// and read it.
package notifications
// Kind names one sort of notification. The set is CHECK-gated in migration
// 0073, swapped since by 0075 (rule 36): a new kind adds its value in a
// migration of the same change, and TestEveryKindPassesTheSchemaChecks
// fails until it does.
type Kind string
const (
KindRequestApproved Kind = "request_approved"
KindRequestRejected Kind = "request_rejected"
KindRequestCompleted Kind = "request_completed"
KindRequestPending Kind = "request_pending"
KindQuarantineFlagged Kind = "quarantine_flagged"
KindScanFailed Kind = "scan_failed"
KindTracksMissing Kind = "tracks_missing"
KindDuplicatesFound Kind = "duplicates_found"
// KindDuplicatesResolved: the duplicate resolver (M498) merged copies on
// its own. The operator asked for that to
// happen automatically; this is how it stays visible.
KindDuplicatesResolved Kind = "duplicates_resolved"
KindPlaybackErrors Kind = "playback_errors"
)
// Audience is who a kind can reach. Admin kinds are never offered to, or
// stored for, a non-admin.
type Audience int
const (
AudienceRequester Audience = iota
AudienceAdmin
)
// EmailGroup is how a kind's emails are grouped. Nothing is emailed per event.
type EmailGroup int
const (
// EmailBatch: one email per batch window, holding everything that
// accumulated since the first un-emailed item.
EmailBatch EmailGroup = iota
// EmailSummary: at most one summary a day, at a set local hour. For new
// music arriving, which comes in bursts and is not urgent.
EmailSummary
)
// Channels are where a kind reaches a user. Inbox is the master switch: with
// it off nothing is stored, so there is nothing for the phone or email to
// deliver. Effective applies that.
type Channels struct {
Inbox bool
Phone bool
Email bool
}
// Effective is what will actually be delivered: phone and email ride on the
// inbox row, so they cannot be on without it.
func (c Channels) Effective() Channels {
return Channels{Inbox: c.Inbox, Phone: c.Inbox && c.Phone, Email: c.Inbox && c.Email}
}
type spec struct {
audience Audience
// coalesce: while one of these is unread, a new event updates it in place
// rather than adding a row. For the burst-prone admin kinds.
coalesce bool
// sumCount: a coalesced update adds the payload's `count` to the unread
// row's, because each event is "N more". Without it the newer payload
// replaces the older, because each event states the whole current total.
sumCount bool
group EmailGroup
defaults Channels
}
var (
allOn = Channels{Inbox: true, Phone: true, Email: true}
healthAlert = Channels{Inbox: true, Phone: true, Email: false}
// inboxOnly: news an admin may want to read but never needs to be
// interrupted for.
inboxOnly = Channels{Inbox: true}
)
var specs = map[Kind]spec{
KindRequestApproved: {audience: AudienceRequester, group: EmailBatch, defaults: allOn},
KindRequestRejected: {audience: AudienceRequester, group: EmailBatch, defaults: allOn},
KindRequestCompleted: {audience: AudienceRequester, group: EmailSummary, defaults: allOn},
KindRequestPending: {audience: AudienceAdmin, group: EmailBatch, defaults: allOn},
KindQuarantineFlagged: {audience: AudienceAdmin, group: EmailBatch, defaults: allOn},
KindScanFailed: {audience: AudienceAdmin, coalesce: true, sumCount: true, group: EmailBatch, defaults: healthAlert},
KindTracksMissing: {audience: AudienceAdmin, coalesce: true, sumCount: true, group: EmailBatch, defaults: healthAlert},
KindDuplicatesFound: {audience: AudienceAdmin, coalesce: true, group: EmailBatch, defaults: healthAlert},
// Each pass reports what it did itself, so the counts add up.
KindDuplicatesResolved: {audience: AudienceAdmin, coalesce: true, sumCount: true, group: EmailBatch, defaults: inboxOnly},
KindPlaybackErrors: {audience: AudienceAdmin, coalesce: true, group: EmailBatch, defaults: healthAlert},
}
// order is the display order for settings screens: the requester's own kinds
// first, then the admin ones.
var order = []Kind{
KindRequestApproved,
KindRequestRejected,
KindRequestCompleted,
KindRequestPending,
KindQuarantineFlagged,
KindScanFailed,
KindTracksMissing,
KindDuplicatesFound,
KindDuplicatesResolved,
KindPlaybackErrors,
}
// Kinds returns every kind in display order.
func Kinds() []Kind { return append([]Kind(nil), order...) }
// Valid reports whether k is a known kind.
func (k Kind) Valid() bool { _, ok := specs[k]; return ok }
// AdminOnly reports whether k reaches only admins.
func (k Kind) AdminOnly() bool { return specs[k].audience == AudienceAdmin }
// Defaults are the channels a user has for k until they change them.
func (k Kind) Defaults() Channels { return specs[k].defaults }
// EmailGroup is how k's emails are grouped.
func (k Kind) EmailGroup() EmailGroup { return specs[k].group }
// coalesceKey is the key k's rows coalesce on, or "" when k never coalesces.
// One key per kind: two unread "tracks missing" rows would only split a count.
func (k Kind) coalesceKey() string {
if specs[k].coalesce {
return string(k)
}
return ""
}