Skip to content

feat(alerts): realtime alert notifications over SMTP + in-app (#25) - #144

Draft
Jovonni wants to merge 2 commits into
masterfrom
feat/realtime-alert-notifications
Draft

feat(alerts): realtime alert notifications over SMTP + in-app (#25)#144
Jovonni wants to merge 2 commits into
masterfrom
feat/realtime-alert-notifications

Conversation

@Jovonni

@Jovonni Jovonni commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

What

Delivers realtime alerts (requested in #25) as email (SMTP) + in-app notifications, wired end-to-end through the pieces we already have — no new tables.

How it reuses existing infrastructure

  • Config: stored in the existing integration_settings table (integration_type="smtp"), loaded with an env-var fallback exactly like ChatService does for LLM providers. smtp is added to the settings whitelist, the password is masked on read, and there's a /test connectivity probe.
  • Delivery point: hooks RuleEngine._fire_alert — the single choke point where every rule-fired alert is created. When the alert node's action is a notify action, it dispatches email + in-app. Best-effort: a mail outage can never block alert creation.
  • In-app: creates rows in the existing notifications table (type alert), so they surface in the notification center already wired into the frontend + notifications router.
  • Rule canvas: the alert node gains a "fire alert + notify" action and an optional recipients field (persisted in flow_graph, read by the engine). No new node type.
  • Settings UI: a new Notifications section with an Email (SMTP) card, driven by the existing data-driven integrations panel.
  • SDK: openuba.send_alert(...) lets Python models raise alerts (and optionally notify). Backed by a new POST /api/v1/alerts that owns SDK alerts under a system SDK Alerts rule (alerts require a rule_id).

Tests (own subsuite, CI-wired)

core/tests/test_services/test_notification_service.py, test_rule_engine_notify.py, and core/tests/test_api_routers/test_settings_smtp.py28 unit tests, all mocked (no SMTP server or DB container): recipient normalization, config load (DB + env), STARTTLS/SSL send paths, error-swallowing, notify-action gating, rule-engine dispatch wiring, and settings whitelist/masking/_test_smtp.

Local: backend suite green, tsc --noEmit clean, next lint clean, next build green.

Closes #25

Adds an SMTP email channel and in-app notifications for detections, wired
through the existing rule engine, rule canvas, settings, and SDK — no new
tables (reuses integration_settings + notifications).

- notification_service: EmailService (config from integration_settings with
  SMTP_* env fallback, à la ChatService) + AlertNotifier (email + per-user
  in-app notification), best-effort so delivery never blocks alerting
- rule_engine._fire_alert dispatches notifications when the alert node action
  is a notify action, threading the node's optional recipients
- settings: register 'smtp' integration, mask the password, add a /test probe
- rule canvas: 'fire alert + notify' action + optional recipients field
- settings UI: Notifications section with an Email (SMTP) integration card
- POST /api/v1/alerts to raise alerts directly (owns them under a system
  'SDK Alerts' rule) with optional notify
- SDK: openuba.send_alert(...) for models
- dedicated test subsuite (28 unit tests, no container needed)

Closes #25
@Jovonni Jovonni mentioned this pull request Aug 13, 2026
The workspace image build failed generating pyspark metadata ('setuptools is
not available in the build environment'). Upgrade build tooling right after the
base image so isolated sdist builds resolve setuptools. Unblocks the workspace
CI triggered by the sdk/ change in this branch.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds realtime alert notifications delivered via SMTP email plus in-app notifications, wiring delivery into the existing alert creation path (rule engine + new API endpoint) and reusing existing persisted settings (integration_settings) and in-app notifications rows.

Changes:

  • Adds SMTP integration configuration (UI + settings API whitelist + /test probe) and a backend notification service to deliver email + in-app notifications.
  • Extends rule-engine alert firing to optionally dispatch notifications (best-effort) and threads optional per-node recipients from the flow graph.
  • Adds an SDK/API surface (openuba.send_alert + POST /api/v1/alerts) to raise alerts programmatically, with optional notification dispatch.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sdk/src/openuba/client.py Adds OpenUBAClient.send_alert() that POSTs alert creation to the API, with optional notify/recipients.
sdk/src/openuba/init.py Exposes top-level send_alert() helper and exports it via __all__.
interface/src/components/settings/settings-tabs.tsx Adds SMTP integration card/fields and a “Notifications” integrations section in Settings UI.
interface/src/components/rules/flow-nodes.tsx Adds “fire alert + notify” action and optional recipients input on the alert node.
docker/workspace/Dockerfile Upgrades pip/setuptools/wheel to support sdist-only dependency installs in workspace image.
core/tests/test_services/test_rule_engine_notify.py Adds unit tests for rule-engine → notifier dispatch gating and best-effort behavior.
core/tests/test_services/test_notification_service.py Adds unit tests for SMTP config loading, recipient normalization, SMTP send paths, and notifier behavior.
core/tests/test_api_routers/test_settings_smtp.py Adds unit tests for SMTP integration whitelist/masking and connectivity testing.
core/services/rule_engine.py Hooks notification dispatch into _fire_alert() for notify actions; threads recipients from flow graph.
core/services/notification_service.py Introduces SMTP email sender + in-app notification creation orchestrator (AlertNotifier).
core/api_schemas/rules.py Adds AlertCreate schema for the new alert creation endpoint.
core/api_routers/settings.py Whitelists smtp, adds _test_smtp(), and masks password in integration configs.
core/api_routers/rules.py Adds POST /alerts for SDK-driven alerts, with optional notification dispatch and SDK-owned rule creation.
Suppressed comments (3)

core/services/notification_service.py:72

  • _flag() uses bool(config[cfg_key]), but if the DB stores booleans as strings (e.g. "false"), bool("false") is truthy and will incorrectly enable TLS/SSL. Parsing string values defensively avoids surprising behavior and makes DB/env handling consistent.
        def _flag(cfg_key: str, env_key: str, default: bool) -> bool:
            if cfg_key in config:
                return bool(config[cfg_key])
            env = os.environ.get(env_key)
            if env is None:

core/services/notification_service.py:80

  • int(config.get("port") or os.environ.get("SMTP_PORT", 587)) can raise ValueError for non-numeric inputs (e.g. mis-set env var), which breaks the contract that send()/config load never raises. Parse the port defensively and default when invalid.
        return {
            "host": host,
            "port": int(config.get("port") or os.environ.get("SMTP_PORT", 587)),
            "username": config.get("username") or os.environ.get("SMTP_USERNAME", ""),
            "password": config.get("password") or os.environ.get("SMTP_PASSWORD", ""),

core/api_routers/settings.py:324

  • Masking password (and api_key) in _mask_sensitive_fields() is good for display, but the Settings UI initializes formData from existing.config and PUTs it back verbatim. That means saving without re-entering the secret can overwrite the stored secret with the masked value (e.g. abcd...wxyz). This existed for api_key already and now also applies to SMTP password.

Consider a coordinated fix: (1) UI: clear password fields on panel open and only include them in the PUT payload when the user entered a new value, and/or (2) API: preserve existing secret on update when the incoming value matches the masking pattern or is empty.

def _mask_sensitive_fields(config: dict) -> dict:
    '''mask api keys / passwords in config for safe display'''
    masked = dict(config)
    for key in ("api_key", "password"):
        if key in masked and masked[key]:

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +58 to +59
if row and row[1]: # enabled
config = dict(row[0]) if row[0] else {}
Comment on lines +296 to +300
port = int(config.get("port") or 587)
use_ssl = bool(config.get("use_ssl", False))
use_tls = bool(config.get("use_tls", True))
username = config.get("username", "")
password = config.get("password", "")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Realtime Alerts

2 participants