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
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>
142 lines
5.4 KiB
Go
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 ""
|
|
}
|