feat(notifications): library health reaches admins, coalesced (#5341)
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m43s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m16s
release / Build signed APK (releases and dev) (push) Successful in 5m27s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m43s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m16s
release / Build signed APK (releases and dev) (push) Successful in 5m27s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
- A failed scan run sends scan_failed. Each failure adds to the count and the notice shows the latest error. A scan cut short by shutdown says nothing. - Marking tracks missing sends tracks_missing with a running count. - A duplicate sweep that proposes a group it had not proposed before sends duplicates_found, counting everything awaiting review. A sweep that only re-finds known groups stays quiet, so a read notice isn't repeated every sweep (CountDuplicateGroupsDetectedSince). - A playback-error report sends playback_errors, counting the unresolved errors (CountUnresolvedPlaybackErrors). The library package gets its notifier as a package-level SetNotifier beside SetEventBus, for the same reason the bus is package-level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,3 +71,16 @@ func (h *handlers) trackLabel(ctx context.Context, trackID pgtype.UUID) string {
|
||||
}
|
||||
return t.Title
|
||||
}
|
||||
|
||||
// notifyPlaybackErrors tells admins how many playback errors await review.
|
||||
// The notice coalesces, so while it is unread a new report only updates the
|
||||
// count.
|
||||
func (h *handlers) notifyPlaybackErrors(ctx context.Context) {
|
||||
n, err := dbq.New(h.pool).CountUnresolvedPlaybackErrors(ctx)
|
||||
if err != nil {
|
||||
h.logger.Warn("api: playback_error: count unresolved", "err", err)
|
||||
return
|
||||
}
|
||||
h.notifier.NotifyLogged(ctx, notifications.KindPlaybackErrors, notifications.ToAdmins(pgtype.UUID{}),
|
||||
notifications.Payload{Count: n}.Map())
|
||||
}
|
||||
|
||||
@@ -210,3 +210,29 @@ func TestQuarantineReasonLabel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotify_PlaybackErrorsCoalesceIntoOneCount(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
|
||||
alice := seedUser(t, pool, "np-play-alice", "pw", false)
|
||||
admin := seedUser(t, pool, "np-play-admin", "pw", true)
|
||||
track := seedQuarantineTrack(t, h, "play")
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
body := fmt.Sprintf(`{"track_id":%q,"kind":"load_failed","client_id":"web-%d"}`, uuidToString(track.ID), i)
|
||||
req := withUser(httptest.NewRequest(http.MethodPost, "/api/playback-errors", strings.NewReader(body)), alice)
|
||||
w := httptest.NewRecorder()
|
||||
h.handleReportPlaybackError(w, req)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("report %d: status = %d; body = %s", i, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
got := inboxOf(t, pool, admin)
|
||||
wantInbox(t, "admin", got, notifications.KindPlaybackErrors)
|
||||
if got[0].Payload.Count != 2 {
|
||||
t.Errorf("count = %d, want 2 unresolved", got[0].Payload.Count)
|
||||
}
|
||||
wantInbox(t, "reporter", inboxOf(t, pool, alice))
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ func (h *handlers) handleReportPlaybackError(w http.ResponseWriter, r *http.Requ
|
||||
writeErr(w, apierror.Internal(err))
|
||||
return
|
||||
}
|
||||
h.notifyPlaybackErrors(r.Context())
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"id": uuidToString(row.ID)})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user