feat: indie status page MVP -- FastAPI + SQLite
- 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
This commit is contained in:
commit
902133edd3
4655 changed files with 1342691 additions and 0 deletions
110
app/services/notifier.py
Normal file
110
app/services/notifier.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue