- 8 DB models (services, incidents, monitors, subscribers, etc.) - Full CRUD API for services, incidents, monitors - Public status page with live data - Incident detail page with timeline - API key authentication - Uptime monitoring scheduler - 13 tests passing - TECHNICAL_DESIGN.md with full spec
110 lines
No EOL
3.1 KiB
Python
110 lines
No EOL
3.1 KiB
Python
"""Notification service — email (SMTP) and webhook dispatch."""
|
|
|
|
import json
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from datetime import datetime
|
|
|
|
import httpx
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.config import settings
|
|
from app.models.models import Incident, NotificationLog, Subscriber
|
|
|
|
|
|
async def send_email_notification(
|
|
to_email: str,
|
|
subject: str,
|
|
body: str,
|
|
) -> bool:
|
|
"""Send an email notification via SMTP. Returns True if successful."""
|
|
if not settings.smtp_host:
|
|
return False
|
|
|
|
msg = MIMEText(body, "html")
|
|
msg["Subject"] = subject
|
|
msg["From"] = settings.smtp_from
|
|
msg["To"] = to_email
|
|
|
|
try:
|
|
with smtplib.SMTP(settings.smtp_host, settings.smtp_port) as server:
|
|
if settings.smtp_user:
|
|
server.starttls()
|
|
server.login(settings.smtp_user, settings.smtp_pass)
|
|
server.send_message(msg)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
async def send_webhook_notification(
|
|
payload: dict,
|
|
) -> bool:
|
|
"""Send a webhook POST notification. Returns True if successful."""
|
|
if not settings.webhook_notify_url:
|
|
return False
|
|
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.post(
|
|
settings.webhook_notify_url,
|
|
json=payload,
|
|
timeout=10.0,
|
|
)
|
|
return response.status_code < 400
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
async def notify_subscribers(
|
|
incident: Incident,
|
|
db: AsyncSession,
|
|
) -> int:
|
|
"""Notify all confirmed subscribers about an incident update. Returns count notified."""
|
|
result = await db.execute(
|
|
select(Subscriber).where(Subscriber.is_confirmed == True) # noqa: E712
|
|
)
|
|
subscribers = result.scalars().all()
|
|
|
|
notified = 0
|
|
subject = f"[{incident.severity.upper()}] {incident.title}"
|
|
|
|
for subscriber in subscribers:
|
|
# Email notification
|
|
email_sent = await send_email_notification(
|
|
to_email=subscriber.email,
|
|
subject=subject,
|
|
body=f"<p>{incident.title}</p><p>Status: {incident.status}</p>",
|
|
)
|
|
if email_sent:
|
|
log = NotificationLog(
|
|
incident_id=incident.id,
|
|
subscriber_id=subscriber.id,
|
|
channel="email",
|
|
status="sent",
|
|
)
|
|
db.add(log)
|
|
notified += 1
|
|
|
|
# Webhook notification
|
|
webhook_sent = await send_webhook_notification(
|
|
payload={
|
|
"incident_id": incident.id,
|
|
"title": incident.title,
|
|
"status": incident.status,
|
|
"severity": incident.severity,
|
|
"started_at": incident.started_at.isoformat() if incident.started_at else None,
|
|
}
|
|
)
|
|
if webhook_sent:
|
|
log = NotificationLog(
|
|
incident_id=incident.id,
|
|
subscriber_id=subscriber.id,
|
|
channel="webhook",
|
|
status="sent",
|
|
)
|
|
db.add(log)
|
|
|
|
await db.flush()
|
|
return notified |