from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.dependencies import get_current_user, get_db from app.models.alert import Alert from app.models.device import Device from app.models.user import User from app.schemas.alert import AlertRead from app.services.sse import SSEEvent, sse_manager router = APIRouter(prefix="/api/v1/alerts", tags=["alerts"]) # SSE channel for all monitor/alert events MONITOR_CHANNEL = "__monitor__" def _to_read(alert: Alert) -> AlertRead: return AlertRead( id=alert.id, device_id=alert.device_id, device_ip=alert.device.ip if alert.device else None, device_hostname=alert.device.hostname if alert.device else None, object_id=alert.device.object_id if alert.device else None, object_name=( alert.device.object.name if alert.device and alert.device.object else None ), status=alert.status, message=alert.message, created_at=alert.created_at, acknowledged_at=alert.acknowledged_at, acknowledged_by_username=( alert.acknowledger.username if alert.acknowledger else None ), resolved_at=alert.resolved_at, ) async def _load_alert(db: AsyncSession, alert_id: int) -> Alert: result = await db.execute( select(Alert) .options( selectinload(Alert.device).selectinload(Device.object), selectinload(Alert.acknowledger), ) .where(Alert.id == alert_id) ) alert = result.scalar_one_or_none() if alert is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found") return alert @router.get("", response_model=list[AlertRead]) async def list_alerts( alert_status: Optional[str] = Query(None, alias="status"), object_id: Optional[int] = Query(None), device_id: Optional[int] = Query(None), limit: int = Query(100, ge=1, le=500), skip: int = Query(0, ge=0), db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): query = ( select(Alert) .options( selectinload(Alert.device).selectinload(Device.object), selectinload(Alert.acknowledger), ) .order_by(Alert.created_at.desc()) .offset(skip) .limit(limit) ) if alert_status and alert_status != "all": query = query.where(Alert.status == alert_status) if device_id is not None: query = query.where(Alert.device_id == device_id) if object_id is not None: query = query.join(Device, Alert.device_id == Device.id).where( Device.object_id == object_id ) result = await db.execute(query) return [_to_read(a) for a in result.scalars().all()] @router.post("/{alert_id}/acknowledge", response_model=AlertRead) async def acknowledge_alert( alert_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): alert = await _load_alert(db, alert_id) if alert.status not in ("open",): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Cannot acknowledge alert with status '{alert.status}'", ) alert.status = "acknowledged" alert.acknowledged_at = datetime.now(timezone.utc) alert.acknowledged_by = current_user.id await db.flush() await db.refresh(alert) # Reload relationships after flush alert = await _load_alert(db, alert_id) result = _to_read(alert) await sse_manager.publish( SSEEvent( type="alert_updated", task_id=MONITOR_CHANNEL, data={ "alert_id": alert_id, "status": "acknowledged", "device_id": alert.device_id, }, ) ) return result @router.post("/{alert_id}/resolve", response_model=AlertRead) async def resolve_alert( alert_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): alert = await _load_alert(db, alert_id) if alert.status == "resolved": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Alert is already resolved", ) alert.status = "resolved" alert.resolved_at = datetime.now(timezone.utc) await db.flush() alert = await _load_alert(db, alert_id) result = _to_read(alert) await sse_manager.publish( SSEEvent( type="alert_updated", task_id=MONITOR_CHANNEL, data={ "alert_id": alert_id, "status": "resolved", "device_id": alert.device_id, }, ) ) return result