feat(notifications): per-user Slack delivery (qbit-slack routing) #31

Merged
KofTwentyTwo merged 2 commits from feature/slack-routing into develop 2026-10-14 16:30:57 +00:00
KofTwentyTwo commented 2026-10-14 16:27:44 +00:00 (Migrated from github.com)

Users can now get notifications in Slack, chosen per category next to In-app and Email. This implements QRun-IO/qbit-slack docs/DESIGN.md section 10. The owner decision of 2026-10-14 supersedes "Slack stays a group or monitor channel only, never per user". docs/NOTIFICATIONS.md is updated to match.

  • Changeset 28:
    • alerts_notification_preference gets slackMode (OFF, IMMEDIATE or DAILY_DIGEST; default OFF) and slackMinSeverity (default WARNING).
    • alerts_notification_recipient gets slackStatus and slackMessageId.
  • NotificationSlackSender runs after the emailer, in the deliver-notifications step.
    • It sends through core SendMessageAction to the provider set by slackProviderName (default slack), addressed user:<id>. There is no qbit-slack import (G1).
    • qbit-slack sends the DM through the recipient's own connection, in their tenant's workspace.
    • Pause and quiet hours reuse AlertRouter.isPaused and AlertTiming.isQuiet; CRITICAL bypasses quiet hours.
    • SECURITY notices send the title and link only. ALERT and muted categories are excluded, as for email.
    • Users on DAILY_DIGEST get one DM at their digest hour, and those rows are marked DIGESTED.
    • Outcomes: no provider gives SKIPPED; a user-facing refusal (for example, Slack isn't connected) gives SKIPPED; other errors give FAILED. The notification itself never fails.
  • RC.6 gaps:
    • There's no idempotency key on the input yet, so slackStatus prevents resends (STAND-IN(core=QRun-IO/qqq#1194, removeBy=4.1.0-RC.8)).
    • slackMessageId stays null until the output carries an id.
    • Availability is checked with getMessagingProvider(name) != null.
  • Tests: NotificationSlackTest has 8 tests: immediate, muted, quiet hours, paused, no provider, errors, SECURITY with no body, and digest.
  • Build: verify passes locally (173 tests; Checkstyle, guardrails and JaCoCo green).

The implementation is by a Sonnet subagent (Codex is unavailable here). I reviewed it.

🤖 Generated with Claude Code

Users can now get notifications in Slack, chosen per category next to In-app and Email. This implements QRun-IO/qbit-slack `docs/DESIGN.md` section 10. The owner decision of 2026-10-14 supersedes "Slack stays a group or monitor channel only, never per user". `docs/NOTIFICATIONS.md` is updated to match. - **Changeset 28:** - `alerts_notification_preference` gets `slackMode` (OFF, IMMEDIATE or DAILY_DIGEST; default OFF) and `slackMinSeverity` (default WARNING). - `alerts_notification_recipient` gets `slackStatus` and `slackMessageId`. - **`NotificationSlackSender`** runs after the emailer, in the deliver-notifications step. - It sends through core `SendMessageAction` to the provider set by `slackProviderName` (default `slack`), addressed `user:<id>`. There is no qbit-slack import (G1). - qbit-slack sends the DM through the recipient's own connection, in their tenant's workspace. - Pause and quiet hours reuse `AlertRouter.isPaused` and `AlertTiming.isQuiet`; CRITICAL bypasses quiet hours. - SECURITY notices send the title and link only. ALERT and muted categories are excluded, as for email. - Users on DAILY_DIGEST get one DM at their digest hour, and those rows are marked DIGESTED. - Outcomes: no provider gives SKIPPED; a user-facing refusal (for example, Slack isn't connected) gives SKIPPED; other errors give FAILED. The notification itself never fails. - **RC.6 gaps:** - There's no idempotency key on the input yet, so `slackStatus` prevents resends (`STAND-IN(core=QRun-IO/qqq#1194, removeBy=4.1.0-RC.8)`). - `slackMessageId` stays null until the output carries an id. - Availability is checked with `getMessagingProvider(name) != null`. - **Tests:** `NotificationSlackTest` has 8 tests: immediate, muted, quiet hours, paused, no provider, errors, SECURITY with no body, and digest. - **Build:** `verify` passes locally (173 tests; Checkstyle, guardrails and JaCoCo green). The implementation is by a Sonnet subagent (Codex is unavailable here). I reviewed it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.