feat(routing): apply quiet hours, user pause and digests (#27, part 1) #30

Merged
KofTwentyTwo merged 2 commits from feature/quiet-hours-digests into develop 2026-10-14 16:17:29 +00:00
KofTwentyTwo commented 2026-10-14 16:13:01 +00:00 (Migrated from github.com)

Part 1 of 3 for #27: quiet hours, user pause and digests. These were stored and editable but never applied. Parts 2 (#29, reminders and escalation) and 3 (#28, teams and mutes) are merged, and this branch has develop merged in.

Routing (design §5.4 order): notify switch, mutes, flapping, visibility, USER_PAUSED, QUIET_HOURS, MIN_SEVERITY, channel disabled, rate limit, duplicate.

  • Quiet hours use the user's timeZone, handle windows that cross midnight (22:00-07:00) and quietDays. Severities at or above quietBypassSeverity (default CRITICAL) break through.
  • Pause (pausedUntil) and quiet hours apply only to push channels (email, email lists, webhooks and the like). The in-app bell is never suppressed, which matches develop's existing pause rule. Channel subscriptions are unaffected.
  • A channel below its minimum severity is now logged as a SUPPRESSED MIN_SEVERITY row instead of being silently skipped.
  • Reminders and escalations (#29) are sent immediately, never digested, but still respect pause and quiet hours on push channels.

Digests:

  • DIGEST_HOURLY and DIGEST_DAILY subscriptions queue DIGEST deliveries, each with a digestKey (recipient, window start, mode) and nextAttemptAt set to the end of the window.
  • The hidden hourly alertsSendDigests job sends one grouped message per user (or channel) per window: hourly, or daily at the user's digestHour in their time zone. It goes through the existing email path (qbit-email's EmailApi when installed) or as one in-app notification.
  • Visibility is checked again at send time, empty digests aren't sent, and durable dispatch claims stop a window from being sent twice. Failed, interrupted or oversized windows (more than 500 events) stay visible as FAILED rows for review rather than being retried automatically, so nobody gets a digest twice.

Schema: guarded changeset 27 (21-26 are taken by #28 and #29).

Tests: mvnw -o -B verify green after merging develop: 165 tests, 94.3% instruction coverage, checkstyle and guardrails pass. The 19 quiet-hours and digest tests cover the window inside and outside, across midnight, quiet days, CRITICAL bypass, pause, the in-app exemption, channel subscriptions, hourly and daily digests in a time zone, grouping, empty digests and an idempotent re-run.

The qbit-slack agent will build Slack on top of this (it reuses AlertTiming.isQuiet). After this merges, qbit-alerts has #23, #26, #27 (all three parts) and #28 for an RC.5.

🤖 Generated with Claude Code

Part 1 of 3 for #27: quiet hours, user pause and digests. These were stored and editable but never applied. Parts 2 (#29, reminders and escalation) and 3 (#28, teams and mutes) are merged, and this branch has develop merged in. **Routing** (design §5.4 order): notify switch, mutes, flapping, visibility, **USER_PAUSED**, **QUIET_HOURS**, **MIN_SEVERITY**, channel disabled, rate limit, duplicate. - Quiet hours use the user's `timeZone`, handle windows that cross midnight (22:00-07:00) and `quietDays`. Severities at or above `quietBypassSeverity` (default CRITICAL) break through. - Pause (`pausedUntil`) and quiet hours apply only to push channels (email, email lists, webhooks and the like). The in-app bell is never suppressed, which matches develop's existing pause rule. Channel subscriptions are unaffected. - A channel below its minimum severity is now logged as a SUPPRESSED `MIN_SEVERITY` row instead of being silently skipped. - Reminders and escalations (#29) are sent immediately, never digested, but still respect pause and quiet hours on push channels. **Digests:** - DIGEST_HOURLY and DIGEST_DAILY subscriptions queue `DIGEST` deliveries, each with a `digestKey` (recipient, window start, mode) and `nextAttemptAt` set to the end of the window. - The hidden hourly `alertsSendDigests` job sends one grouped message per user (or channel) per window: hourly, or daily at the user's `digestHour` in their time zone. It goes through the existing email path (qbit-email's EmailApi when installed) or as one in-app notification. - Visibility is checked again at send time, empty digests aren't sent, and durable dispatch claims stop a window from being sent twice. Failed, interrupted or oversized windows (more than 500 events) stay visible as FAILED rows for review rather than being retried automatically, so nobody gets a digest twice. **Schema:** guarded changeset **27** (21-26 are taken by #28 and #29). **Tests:** `mvnw -o -B verify` green after merging develop: 165 tests, 94.3% instruction coverage, checkstyle and guardrails pass. The 19 quiet-hours and digest tests cover the window inside and outside, across midnight, quiet days, CRITICAL bypass, pause, the in-app exemption, channel subscriptions, hourly and daily digests in a time zone, grouping, empty digests and an idempotent re-run. The qbit-slack agent will build Slack on top of this (it reuses `AlertTiming.isQuiet`). After this merges, qbit-alerts has #23, #26, #27 (all three parts) and #28 for an RC.5. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.