feat: notifications say what changed and alert once: a grouped notice names the tracks, grows without re-alerting, and a phone pile lists its titles (M542 #6146-#6151)
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m33s
release / android (push) Failing after 1m42s
release / integration (push) Failing after 4m42s
release / Build signed APK (releases and dev) (push) Successful in 4m55s
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

- An update to a grouped notice keeps its arrival time and email state, so the
  phone and the email no longer go off again each time it grows.
- Notices carry the names of what changed (newest ten), shown as three and
  "and N more": tracks still missing after the scan's move/replacement fold,
  duplicate groups new to a sweep, songs merged automatically, and the track
  and listener a playback error was for.
- Duplicates found counts only the groups new to that sweep; playback errors
  count reports, not the unresolved total.
- A scan failure says what stopped and links to Admin > Library.
- Android: a pile of more than three alerts lists the newest five titles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 22:44:19 -04:00
co-authored by Claude Opus 5.5
parent 2365514e5b
commit 854c307834
26 changed files with 486 additions and 190 deletions
+27 -5
View File
@@ -146,19 +146,41 @@ func TestNotify_CoalescedCountAddsUpWhileUnread_AndStartsAfreshOnceRead(t *testi
require.True(t, rows[1].ReadAt.Valid)
}
func TestNotify_CoalescedTotalReplacesWhenEventsStateTheWhole(t *testing.T) {
// M542: an update says more, newest names first, without arriving again. A
// moved created_at was announced again on the phone, and a cleared emailed_at
// was emailed again.
func TestNotify_ACoalescedUpdateMergesNamesAndKeepsItsArrival(t *testing.T) {
pool := testPool(t)
q := dbq.New(pool)
ctx := context.Background()
admin := mkUser(t, q, "admin", true)
n := notifications.New(pool, nil, nil)
to := notifications.ToUser(admin)
notify := func(items ...string) {
t.Helper()
require.NoError(t, n.Notify(ctx, notifications.KindDuplicatesFound, to,
notifications.Payload{Count: int64(len(items)), Items: items}.Map()))
}
// duplicates_found states the pending total each time; it must not sum.
require.NoError(t, n.Notify(ctx, notifications.KindDuplicatesFound, notifications.ToUser(admin), map[string]any{"count": 5}))
require.NoError(t, n.Notify(ctx, notifications.KindDuplicatesFound, notifications.ToUser(admin), map[string]any{"count": 3}))
notify("A", "B")
first := rowsFor(t, q, admin)
require.Len(t, first, 1)
_, err := pool.Exec(ctx, `UPDATE user_notifications SET emailed_at = now() WHERE id = $1`, first[0].ID)
require.NoError(t, err)
notify("C", "D", "E", "F", "G", "H", "I", "J", "K")
rows := rowsFor(t, q, admin)
require.Len(t, rows, 1)
require.Equal(t, int64(3), count(t, rows[0].Payload))
var p notifications.Payload
require.NoError(t, json.Unmarshal(rows[0].Payload, &p))
require.Equal(t, int64(11), p.Count)
require.Equal(t, []string{"C", "D", "E", "F", "G", "H", "I", "J", "K", "A"}, p.Items,
"the newest names first, ten kept")
require.True(t, rows[0].CreatedAt.Time.Equal(first[0].CreatedAt.Time), "an update is not a new arrival")
var emailed pgtype.Timestamptz
require.NoError(t, pool.QueryRow(ctx, `SELECT emailed_at FROM user_notifications WHERE id = $1`, rows[0].ID).Scan(&emailed))
require.True(t, emailed.Valid, "an emailed notice is not emailed again because it grew")
}
func TestNotify_RequestKindsNeverCoalesce(t *testing.T) {