"""Alert management API routes.""" from datetime import datetime, timezone from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models.alert import AlertRule, AlertHistory, AlertLevel, AlertOperator from app.schemas.alert import ( AlertRuleCreate, AlertRuleUpdate, AlertRuleResponse, AlertHistoryResponse, ) router = APIRouter(prefix="/api/alerts", tags=["alerts"]) # ── Rule CRUD ────────────────────────────────────────────────── @router.post("/rules", response_model=AlertRuleResponse, status_code=201) async def create_alert_rule( data: AlertRuleCreate, db: AsyncSession = Depends(get_db), ): """Create a new alert rule.""" rule = AlertRule( project_id=data.project_id, name=data.name, level=data.level, field=data.field, operator=data.operator, threshold=data.threshold, window_minutes=data.window_minutes, notify_channels=",".join(data.notify_channels) if data.notify_channels else "", ) db.add(rule) await db.flush() await db.refresh(rule) return _rule_to_response(rule) @router.get("/rules", response_model=List[AlertRuleResponse]) async def list_alert_rules( project_id: Optional[str] = Query(None), db: AsyncSession = Depends(get_db), ): """List alert rules, optionally filtered by project.""" stmt = select(AlertRule) if project_id: stmt = stmt.where(AlertRule.project_id == project_id) stmt = stmt.order_by(AlertRule.created_at.desc()) result = await db.execute(stmt) return [_rule_to_response(r) for r in result.scalars().all()] @router.get("/rules/{rule_id}", response_model=AlertRuleResponse) async def get_alert_rule(rule_id: str, db: AsyncSession = Depends(get_db)): """Get a single alert rule.""" result = await db.execute(select(AlertRule).where(AlertRule.id == rule_id)) rule = result.scalar_one_or_none() if not rule: raise HTTPException(status_code=404, detail="Alert rule not found") return _rule_to_response(rule) @router.patch("/rules/{rule_id}", response_model=AlertRuleResponse) async def update_alert_rule( rule_id: str, data: AlertRuleUpdate, db: AsyncSession = Depends(get_db), ): """Update an alert rule.""" result = await db.execute(select(AlertRule).where(AlertRule.id == rule_id)) rule = result.scalar_one_or_none() if not rule: raise HTTPException(status_code=404, detail="Alert rule not found") if data.name is not None: rule.name = data.name if data.level is not None: rule.level = data.level if data.field is not None: rule.field = data.field if data.operator is not None: rule.operator = data.operator if data.threshold is not None: rule.threshold = data.threshold if data.window_minutes is not None: rule.window_minutes = data.window_minutes if data.is_enabled is not None: rule.is_enabled = data.is_enabled if data.notify_channels is not None: rule.notify_channels = ",".join(data.notify_channels) await db.flush() await db.refresh(rule) return _rule_to_response(rule) @router.delete("/rules/{rule_id}", status_code=204) async def delete_alert_rule(rule_id: str, db: AsyncSession = Depends(get_db)): """Delete an alert rule.""" result = await db.execute(select(AlertRule).where(AlertRule.id == rule_id)) rule = result.scalar_one_or_none() if not rule: raise HTTPException(status_code=404, detail="Alert rule not found") await db.delete(rule) # ── Alert History ────────────────────────────────────────────── @router.get("/history", response_model=List[AlertHistoryResponse]) async def list_alert_history( project_id: Optional[str] = Query(None), rule_id: Optional[str] = Query(None), limit: int = Query(100, ge=1, le=1000), db: AsyncSession = Depends(get_db), ): """List alert trigger history.""" stmt = select(AlertHistory).order_by(AlertHistory.triggered_at.desc()).limit(limit) if project_id: stmt = stmt.where(AlertHistory.project_id == project_id) if rule_id: stmt = stmt.where(AlertHistory.rule_id == rule_id) result = await db.execute(stmt) return [_history_to_response(h) for h in result.scalars().all()] @router.post("/history/{history_id}/acknowledge", response_model=AlertHistoryResponse) async def acknowledge_alert( history_id: str, db: AsyncSession = Depends(get_db), ): """Mark an alert as acknowledged.""" result = await db.execute(select(AlertHistory).where(AlertHistory.id == history_id)) history = result.scalar_one_or_none() if not history: raise HTTPException(status_code=404, detail="Alert history not found") history.is_acknowledged = True await db.flush() await db.refresh(history) return _history_to_response(history) # ── Helpers ──────────────────────────────────────────────────── def _rule_to_response(rule: AlertRule) -> AlertRuleResponse: return AlertRuleResponse( id=rule.id, project_id=rule.project_id, name=rule.name, level=rule.level.value if hasattr(rule.level, "value") else rule.level, field=rule.field, operator=rule.operator.value if hasattr(rule.operator, "value") else rule.operator, threshold=rule.threshold, window_minutes=rule.window_minutes, is_enabled=rule.is_enabled, notify_channels=rule.notify_channels.split(",") if rule.notify_channels else [], created_at=rule.created_at, ) def _history_to_response(history: AlertHistory) -> AlertHistoryResponse: return AlertHistoryResponse( id=history.id, rule_id=history.rule_id, project_id=history.project_id, level=history.level.value if hasattr(history.level, "value") else history.level, message=history.message, triggered_value=history.triggered_value, is_acknowledged=history.is_acknowledged, triggered_at=history.triggered_at, )