Quieter notifications: quiet resolves, per-type reopen window, self-clearing overflow alert #48

Open
KofTwentyTwo wants to merge 5 commits from feature/quieter-notifications into develop
pull from: feature/quieter-notifications
KofTwentyTwo commented 2026-10-16 20:52:32 +00:00 (Migrated from github.com)

Part of KofTwentyTwo/Website-Backend#306

Notifications should be things a person needs to see or act on. This PR removes five sources of noise, one commit each.

Changes

  1. A resolve no longer marks the bell entry unread. The in-app channel updates the alert's existing bell entry in place: the title becomes "Resolved: ..." and the read state stays as it was. A resolve never adds a bell entry for someone who did not have one. A subscription whose events name RESOLVED and differ from the default set (AlertRouter.DEFAULT_SUBSCRIPTION_EVENTS, now shared by the three places that hard-coded it) still gets the old unread behavior. Email, Slack and webhook resolve notices are unchanged. If a reopen is suppressed as a duplicate (10-minute window), the bell entry's text is still refreshed, so it never keeps saying "Resolved" while the alert is open.

  2. Per-type reopen window. A raise of the same (type, dedupKey) reopens the previous alert, and its existing bell entry, within:

    • the type's alerts_type.reopenWindowMinutes (new nullable column, changeset 55, H2 test DDL, entity, Tuning section, help); otherwise
    • the longest matching prefix in AlertsQBitConfig.reopenWindowMinutesByTypePrefix (default health. = 360 minutes; validated); otherwise
    • reopenWindowMinutes (30, unchanged).

    Flapping detection is unchanged: reopens still count toward flapThreshold within flapWindowMinutes.

  3. Sink overflow alert clears itself. alerts.sink.overflow now has a stable dedup key. It is the key earlier versions used implicitly, so an alert they left open also resolves. The drain thread resolves it once the queue has stayed below half full for 60 seconds, then re-arms the flag so a later overflow raises it again. Still ERROR and visible to admins only. The declared resolve mode is PRODUCER. Existing type rows keep their stored label, and the behavior does not depend on it.

  4. "API key created" notice removed (AlertsNotices.apiKeyCreated). No callers were found outside this repo. The API key expiry, access-changed and share notices stay. "New sign-in" is unchanged: the qbit has no sign-in, device or IP history (core has no sign-in events, and newSignIn receives only a display string), so it cannot tell a new device from a known one. The host has to make that call. The Javadoc and docs now say to call it only for a device or address not seen recently.

  5. Job notices only for runs nobody was watching. AlertsProcessListener can tell from RunProcessInput.getFrontendStepBehavior(). The process screens run with BREAK and end on the result screen. Scheduled, API, queue, ESB and automation runs use SKIP (or FAIL). Only non-BREAK runs notify now.

Before / after

Notification or alert type Before After
Alert RAISED (in-app bell) New bell entry, unread Unchanged
Alert REOPENED (in-app) Same bell entry back to unread (only within 30 min; later a new alert and a second bell entry) Same alert and bell entry back to unread, within the per-type window (health.* 6 h, others 30 min, per-type override)
Alert SEVERITY_CHANGED up (in-app) Bell entry back to unread Unchanged
Alert FLAPPING_STARTED (in-app) One notice, then flapping suppression Unchanged
Alert RESOLVED (in-app bell) Bell entry reset to unread Entry updated in place ("Resolved: ..."), read state kept, nothing added; old behavior only for subscriptions that chose RESOLVED explicitly (non-default event list)
Alert RESOLVED (email / Slack / webhook) Sent per subscription events Unchanged
Alert ESCALATED / REMINDER Sent Unchanged
alerts.sink.overflow "Alert signals are being dropped" No dedup key, manual resolve, raised once per process lifetime, open forever Stable dedup key; auto-resolves after 60 s below 50% queue capacity; re-raises on a later overflow; ERROR, admins
alerts.producer.flood Quiet-period resolve Unchanged
alerts.monitor.failing, alerts.channel.failing Producer resolve Unchanged
SECURITY "New sign-in to your account" Sent whenever the host calls it Unchanged (no device/IP data in the qbit; the host decides), docs clarified
SECURITY "API key created" Sent to the person who created the key Removed
SECURITY "API key expires ..." Sent, one per key Unchanged
SECURITY "Your access changed" Sent Unchanged
SHARE (shared / assigned) Sent Unchanged
RELEASE "Version X is out" Sent to all users Unchanged
JOB " failed" Sent to whoever started any run Only for non-interactive runs (scheduled, background, API, queue)
JOB " finished with errors" Sent to whoever started any run Only for non-interactive runs

Tests

  • QuietNotificationsTest: a resolve keeps the entry read and marks it resolved; an unread entry stays unread; a resolve adds no entry; an explicit RESOLVED subscription still gets unread; a quick reopen restores the title; a health blip 2 h later reopens the same alert and bell entry, 7 h later it creates a new one, and other types keep 30 min; a per-type override wins both ways; prefix resolution (longest wins) and validation.
  • SinkOverflowTest: the alert is raised once with the dedup key; the drain must last 60 s (a new overflow restarts the wait); it then resolves and re-arms; a later overflow raises it again.
  • NotificationEmailAndNoticesTest: no "API key created" notice.
  • JobNotificationsTest: an interactive (BREAK) failed run sends nothing; isInteractive cases.
  • ./mvnw verify passes locally (checkstyle, guardrails, JaCoCo gate).

CHANGELOG and UPGRADE are updated. Hosts calling apiKeyCreated need to delete the call.

🤖 Generated with Claude Code

Part of KofTwentyTwo/Website-Backend#306 Notifications should be things a person needs to see or act on. This PR removes five sources of noise, one commit each. ## Changes 1. **A resolve no longer marks the bell entry unread.** The in-app channel updates the alert's existing bell entry in place: the title becomes "Resolved: ..." and the read state stays as it was. A resolve never adds a bell entry for someone who did not have one. A subscription whose `events` name RESOLVED and differ from the default set (`AlertRouter.DEFAULT_SUBSCRIPTION_EVENTS`, now shared by the three places that hard-coded it) still gets the old unread behavior. Email, Slack and webhook resolve notices are unchanged. If a reopen is suppressed as a duplicate (10-minute window), the bell entry's text is still refreshed, so it never keeps saying "Resolved" while the alert is open. 2. **Per-type reopen window.** A raise of the same (type, dedupKey) reopens the previous alert, and its existing bell entry, within: - the type's `alerts_type.reopenWindowMinutes` (new nullable column, changeset 55, H2 test DDL, entity, Tuning section, help); otherwise - the longest matching prefix in `AlertsQBitConfig.reopenWindowMinutesByTypePrefix` (default `health.` = 360 minutes; validated); otherwise - `reopenWindowMinutes` (30, unchanged). Flapping detection is unchanged: reopens still count toward `flapThreshold` within `flapWindowMinutes`. 3. **Sink overflow alert clears itself.** `alerts.sink.overflow` now has a stable dedup key. It is the key earlier versions used implicitly, so an alert they left open also resolves. The drain thread resolves it once the queue has stayed below half full for 60 seconds, then re-arms the flag so a later overflow raises it again. Still ERROR and visible to admins only. The declared resolve mode is PRODUCER. Existing type rows keep their stored label, and the behavior does not depend on it. 4. **"API key created" notice removed** (`AlertsNotices.apiKeyCreated`). No callers were found outside this repo. The API key expiry, access-changed and share notices stay. **"New sign-in" is unchanged:** the qbit has no sign-in, device or IP history (core has no sign-in events, and `newSignIn` receives only a display string), so it cannot tell a new device from a known one. The host has to make that call. The Javadoc and docs now say to call it only for a device or address not seen recently. 5. **Job notices only for runs nobody was watching.** `AlertsProcessListener` can tell from `RunProcessInput.getFrontendStepBehavior()`. The process screens run with BREAK and end on the result screen. Scheduled, API, queue, ESB and automation runs use SKIP (or FAIL). Only non-BREAK runs notify now. ## Before / after | Notification or alert type | Before | After | |---|---|---| | Alert RAISED (in-app bell) | New bell entry, unread | Unchanged | | Alert REOPENED (in-app) | Same bell entry back to unread (only within 30 min; later a new alert and a second bell entry) | Same alert and bell entry back to unread, within the per-type window (`health.*` 6 h, others 30 min, per-type override) | | Alert SEVERITY_CHANGED up (in-app) | Bell entry back to unread | Unchanged | | Alert FLAPPING_STARTED (in-app) | One notice, then flapping suppression | Unchanged | | Alert RESOLVED (in-app bell) | Bell entry reset to unread | Entry updated in place ("Resolved: ..."), read state kept, nothing added; old behavior only for subscriptions that chose RESOLVED explicitly (non-default event list) | | Alert RESOLVED (email / Slack / webhook) | Sent per subscription events | Unchanged | | Alert ESCALATED / REMINDER | Sent | Unchanged | | `alerts.sink.overflow` "Alert signals are being dropped" | No dedup key, manual resolve, raised once per process lifetime, open forever | Stable dedup key; auto-resolves after 60 s below 50% queue capacity; re-raises on a later overflow; ERROR, admins | | `alerts.producer.flood` | Quiet-period resolve | Unchanged | | `alerts.monitor.failing`, `alerts.channel.failing` | Producer resolve | Unchanged | | SECURITY "New sign-in to your account" | Sent whenever the host calls it | Unchanged (no device/IP data in the qbit; the host decides), docs clarified | | SECURITY "API key created" | Sent to the person who created the key | Removed | | SECURITY "API key expires ..." | Sent, one per key | Unchanged | | SECURITY "Your access changed" | Sent | Unchanged | | SHARE (shared / assigned) | Sent | Unchanged | | RELEASE "Version X is out" | Sent to all users | Unchanged | | JOB "<process> failed" | Sent to whoever started any run | Only for non-interactive runs (scheduled, background, API, queue) | | JOB "<process> finished with errors" | Sent to whoever started any run | Only for non-interactive runs | ## Tests - `QuietNotificationsTest`: a resolve keeps the entry read and marks it resolved; an unread entry stays unread; a resolve adds no entry; an explicit RESOLVED subscription still gets unread; a quick reopen restores the title; a health blip 2 h later reopens the same alert and bell entry, 7 h later it creates a new one, and other types keep 30 min; a per-type override wins both ways; prefix resolution (longest wins) and validation. - `SinkOverflowTest`: the alert is raised once with the dedup key; the drain must last 60 s (a new overflow restarts the wait); it then resolves and re-arms; a later overflow raises it again. - `NotificationEmailAndNoticesTest`: no "API key created" notice. - `JobNotificationsTest`: an interactive (BREAK) failed run sends nothing; `isInteractive` cases. - `./mvnw verify` passes locally (checkstyle, guardrails, JaCoCo gate). CHANGELOG and UPGRADE are updated. Hosts calling `apiKeyCreated` need to delete the call. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Checking for merge conflicts…
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feature/quieter-notifications:feature/quieter-notifications
git checkout feature/quieter-notifications
Sign in to join this conversation.