Skip to content

Commit 2ff5bc2

Browse files
springdomclaude
andcommitted
feat: add Teams, Webhook, and PagerDuty notification channels
Add 3 new notification channel types alongside existing Slack and Email: - Microsoft Teams (Adaptive Card via incoming webhook / Power Automate) - Generic Outbound Webhook (JSON payload with optional shared secret) - PagerDuty (Events API v2 with trigger/resolve/dedup_key sync) Also fixes the Test button (missing selectinload for incident.alerts), updates frontend channel form to support all 5 types with dynamic config fields, adds 15 new unit tests, and rewrites README with comprehensive feature documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 982a79d commit 2ff5bc2

8 files changed

Lines changed: 870 additions & 154 deletions

File tree

README.md

Lines changed: 277 additions & 77 deletions
Large diffs are not rendered by default.

backend/api/routes/notifications.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from fastapi import APIRouter, Depends, HTTPException, Query
66
from sqlalchemy import func, select
77
from sqlalchemy.ext.asyncio import AsyncSession
8+
from sqlalchemy.orm import selectinload
89

910
from backend.database import get_db
1011
from backend.models import (
@@ -64,16 +65,23 @@ async def create_channel(
6465
try:
6566
channel_type = ChannelType(data.channel_type)
6667
except ValueError:
68+
valid = ", ".join(f"'{t.value}'" for t in ChannelType)
6769
raise HTTPException(
6870
400,
69-
f"Invalid channel_type: {data.channel_type}. Must be 'slack' or 'email'",
71+
f"Invalid channel_type: {data.channel_type}. Must be one of: {valid}",
7072
)
7173

7274
# Validate config based on type
7375
if channel_type == ChannelType.SLACK and not data.config.get("webhook_url"):
7476
raise HTTPException(400, "Slack channels require 'webhook_url' in config")
7577
if channel_type == ChannelType.EMAIL and not data.config.get("recipients"):
7678
raise HTTPException(400, "Email channels require 'recipients' list in config")
79+
if channel_type == ChannelType.TEAMS and not data.config.get("webhook_url"):
80+
raise HTTPException(400, "Teams channels require 'webhook_url' in config")
81+
if channel_type == ChannelType.WEBHOOK and not data.config.get("webhook_url"):
82+
raise HTTPException(400, "Webhook channels require 'webhook_url' in config")
83+
if channel_type == ChannelType.PAGERDUTY and not data.config.get("routing_key"):
84+
raise HTTPException(400, "PagerDuty channels require 'routing_key' in config")
7785

7886
channel = NotificationChannel(
7987
name=data.name,
@@ -153,25 +161,25 @@ async def test_channel(
153161
if not channel:
154162
raise HTTPException(404, "Channel not found")
155163

156-
# Find a recent incident to use as test data, or create a mock
164+
# Find a recent incident to use as test data, eagerly load alerts
157165
stmt = (
158166
select(Incident)
167+
.options(selectinload(Incident.alerts))
159168
.order_by(Incident.created_at.desc())
160169
.limit(1)
161170
)
162171
result = await db.execute(stmt)
163-
incident = result.scalar_one_or_none()
172+
incident = result.unique().scalar_one_or_none()
164173

165174
if not incident:
166175
return {"status": "error", "message": "No incidents available for test notification"}
167176

168177
try:
169-
if channel.channel_type == ChannelType.SLACK:
170-
from backend.core.notifications import _send_slack
171-
await _send_slack(channel, incident, "incident_created")
172-
elif channel.channel_type == ChannelType.EMAIL:
173-
from backend.core.notifications import _send_email
174-
await _send_email(channel, incident, "incident_created")
178+
from backend.core.notifications import _SENDERS
179+
sender = _SENDERS.get(channel.channel_type)
180+
if not sender:
181+
return {"status": "error", "message": f"Unknown channel type: {channel.channel_type}"}
182+
await sender(channel, incident, "incident_created")
175183
return {"status": "sent"}
176184
except Exception as e:
177185
return {"status": "error", "message": str(e)}

0 commit comments

Comments
 (0)