feat: webhook notification channel with UI and ingest dispatch
Add webhook config (DB/env), JSON POST on events/problems, settings API, Settings UI section, and notify_dispatch for multi-channel ingest. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ from app.models.notification_channel import NotificationChannel
|
||||
|
||||
VALID_SEVERITIES = frozenset({"info", "warning", "high", "critical"})
|
||||
CHANNEL_TELEGRAM = "telegram"
|
||||
CHANNEL_WEBHOOK = "webhook"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -31,6 +32,24 @@ class TelegramConfig:
|
||||
return self.enabled and self.configured
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WebhookConfig:
|
||||
enabled: bool
|
||||
url: str
|
||||
secret_header: str
|
||||
secret: str
|
||||
min_severity: str
|
||||
source: str # env | db
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.url.strip())
|
||||
|
||||
@property
|
||||
def active(self) -> bool:
|
||||
return self.enabled and self.configured
|
||||
|
||||
|
||||
def _telegram_from_env() -> TelegramConfig:
|
||||
settings = get_settings()
|
||||
return TelegramConfig(
|
||||
@@ -107,3 +126,88 @@ def upsert_telegram_channel(
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return get_effective_telegram_config(db)
|
||||
|
||||
|
||||
def _webhook_from_env() -> WebhookConfig:
|
||||
settings = get_settings()
|
||||
return WebhookConfig(
|
||||
enabled=bool(settings.webhook_enabled),
|
||||
url=settings.webhook_url.strip(),
|
||||
secret_header=settings.webhook_secret_header.strip(),
|
||||
secret=settings.webhook_secret.strip(),
|
||||
min_severity=settings.webhook_min_severity.strip() or "high",
|
||||
source="env",
|
||||
)
|
||||
|
||||
|
||||
def get_effective_webhook_config(db: Session | None = None) -> WebhookConfig:
|
||||
if db is None:
|
||||
from app.database import SessionLocal
|
||||
|
||||
session = SessionLocal()
|
||||
try:
|
||||
return get_effective_webhook_config(session)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_WEBHOOK))
|
||||
env_cfg = _webhook_from_env()
|
||||
if row is None:
|
||||
return env_cfg
|
||||
|
||||
url = (row.webhook_url or "").strip() or env_cfg.url
|
||||
secret_header = (row.webhook_secret_header or "").strip() or env_cfg.secret_header
|
||||
secret = (row.webhook_secret or "").strip() or env_cfg.secret
|
||||
min_sev = (row.min_severity or "").strip() or env_cfg.min_severity
|
||||
if min_sev not in VALID_SEVERITIES:
|
||||
min_sev = env_cfg.min_severity
|
||||
|
||||
return WebhookConfig(
|
||||
enabled=bool(row.enabled),
|
||||
url=url,
|
||||
secret_header=secret_header,
|
||||
secret=secret,
|
||||
min_severity=min_sev,
|
||||
source="db",
|
||||
)
|
||||
|
||||
|
||||
def upsert_webhook_channel(
|
||||
db: Session,
|
||||
*,
|
||||
enabled: bool | None = None,
|
||||
url: str | None = None,
|
||||
secret_header: str | None = None,
|
||||
secret: str | None = None,
|
||||
min_severity: str | None = None,
|
||||
) -> WebhookConfig:
|
||||
row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_WEBHOOK))
|
||||
env_cfg = _webhook_from_env()
|
||||
|
||||
if row is None:
|
||||
row = NotificationChannel(
|
||||
channel=CHANNEL_WEBHOOK,
|
||||
enabled=env_cfg.enabled,
|
||||
min_severity=env_cfg.min_severity,
|
||||
webhook_url=env_cfg.url or None,
|
||||
webhook_secret_header=env_cfg.secret_header or None,
|
||||
webhook_secret=env_cfg.secret or None,
|
||||
)
|
||||
db.add(row)
|
||||
|
||||
if enabled is not None:
|
||||
row.enabled = enabled
|
||||
if min_severity is not None:
|
||||
if min_severity not in VALID_SEVERITIES:
|
||||
raise ValueError(f"invalid min_severity: {min_severity}")
|
||||
row.min_severity = min_severity
|
||||
if url is not None and url.strip():
|
||||
row.webhook_url = url.strip()
|
||||
if secret_header is not None:
|
||||
row.webhook_secret_header = secret_header.strip() or None
|
||||
if secret is not None and secret.strip():
|
||||
row.webhook_secret = secret.strip()
|
||||
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return get_effective_webhook_config(db)
|
||||
|
||||
Reference in New Issue
Block a user