""" Alerts Router API endpoints for managing in-app alerts. """ from fastapi import APIRouter, Request, Depends, HTTPException, Query from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, JSONResponse from sqlalchemy.orm import Session from typing import Optional from datetime import datetime, timedelta from backend.database import get_db from backend.models import Alert, RosterUnit from backend.services.alert_service import get_alert_service router = APIRouter(prefix="/api/alerts", tags=["alerts"]) templates = Jinja2Templates(directory="templates") # ============================================================================ # Alert List and Count # ============================================================================ @router.get("/") async def list_alerts( db: Session = Depends(get_db), status: Optional[str] = Query(None, description="Filter by status: active, acknowledged, resolved, dismissed"), project_id: Optional[str] = Query(None), unit_id: Optional[str] = Query(None), alert_type: Optional[str] = Query(None, description="Filter by type: device_offline, device_online, schedule_failed"), limit: int = Query(50, le=100), offset: int = Query(0, ge=0), ): """ List alerts with optional filters. """ alert_service = get_alert_service(db) alerts = alert_service.get_all_alerts( status=status, project_id=project_id, unit_id=unit_id, alert_type=alert_type, limit=limit, offset=offset, ) return { "alerts": [ { "id": a.id, "alert_type": a.alert_type, "severity": a.severity, "title": a.title, "message": a.message, "status": a.status, "unit_id": a.unit_id, "project_id": a.project_id, "location_id": a.location_id, "created_at": a.created_at.isoformat() if a.created_at else None, "acknowledged_at": a.acknowledged_at.isoformat() if a.acknowledged_at else None, "resolved_at": a.resolved_at.isoformat() if a.resolved_at else None, } for a in alerts ], "count": len(alerts), "limit": limit, "offset": offset, } @router.get("/active") async def list_active_alerts( db: Session = Depends(get_db), project_id: Optional[str] = Query(None), unit_id: Optional[str] = Query(None), alert_type: Optional[str] = Query(None), min_severity: Optional[str] = Query(None, description="Minimum severity: info, warning, critical"), limit: int = Query(50, le=100), ): """ List only active alerts. """ alert_service = get_alert_service(db) alerts = alert_service.get_active_alerts( project_id=project_id, unit_id=unit_id, alert_type=alert_type, min_severity=min_severity, limit=limit, ) return { "alerts": [ { "id": a.id, "alert_type": a.alert_type, "severity": a.severity, "title": a.title, "message": a.message, "unit_id": a.unit_id, "project_id": a.project_id, "created_at": a.created_at.isoformat() if a.created_at else None, } for a in alerts ], "count": len(alerts), } @router.get("/active/count") async def get_active_alert_count(db: Session = Depends(get_db)): """ Get count of active alerts (for navbar badge). """ alert_service = get_alert_service(db) count = alert_service.get_active_alert_count() return {"count": count} # ============================================================================ # Single Alert Operations # ============================================================================ @router.get("/{alert_id}") async def get_alert( alert_id: str, db: Session = Depends(get_db), ): """ Get a specific alert. """ alert = db.query(Alert).filter_by(id=alert_id).first() if not alert: raise HTTPException(status_code=404, detail="Alert not found") # Get related unit info unit = None if alert.unit_id: unit = db.query(RosterUnit).filter_by(id=alert.unit_id).first() return { "id": alert.id, "alert_type": alert.alert_type, "severity": alert.severity, "title": alert.title, "message": alert.message, "metadata": alert.alert_metadata, "status": alert.status, "unit_id": alert.unit_id, "unit_name": unit.id if unit else None, "project_id": alert.project_id, "location_id": alert.location_id, "schedule_id": alert.schedule_id, "created_at": alert.created_at.isoformat() if alert.created_at else None, "acknowledged_at": alert.acknowledged_at.isoformat() if alert.acknowledged_at else None, "resolved_at": alert.resolved_at.isoformat() if alert.resolved_at else None, "expires_at": alert.expires_at.isoformat() if alert.expires_at else None, } @router.post("/{alert_id}/acknowledge") async def acknowledge_alert( alert_id: str, db: Session = Depends(get_db), ): """ Mark alert as acknowledged. """ alert_service = get_alert_service(db) alert = alert_service.acknowledge_alert(alert_id) if not alert: raise HTTPException(status_code=404, detail="Alert not found") return { "success": True, "alert_id": alert.id, "status": alert.status, } @router.post("/{alert_id}/dismiss") async def dismiss_alert( alert_id: str, db: Session = Depends(get_db), ): """ Dismiss alert. """ alert_service = get_alert_service(db) alert = alert_service.dismiss_alert(alert_id) if not alert: raise HTTPException(status_code=404, detail="Alert not found") return { "success": True, "alert_id": alert.id, "status": alert.status, } @router.post("/{alert_id}/resolve") async def resolve_alert( alert_id: str, db: Session = Depends(get_db), ): """ Manually resolve an alert. """ alert_service = get_alert_service(db) alert = alert_service.resolve_alert(alert_id) if not alert: raise HTTPException(status_code=404, detail="Alert not found") return { "success": True, "alert_id": alert.id, "status": alert.status, } # ============================================================================ # HTML Partials for HTMX # ============================================================================ @router.get("/partials/dropdown", response_class=HTMLResponse) async def get_alert_dropdown( request: Request, db: Session = Depends(get_db), ): """ Return HTML partial for alert dropdown in navbar. """ alert_service = get_alert_service(db) alerts = alert_service.get_active_alerts(limit=10) # Calculate relative time for each alert now = datetime.utcnow() alerts_data = [] for alert in alerts: delta = now - alert.created_at if delta.days > 0: time_ago = f"{delta.days}d ago" elif delta.seconds >= 3600: time_ago = f"{delta.seconds // 3600}h ago" elif delta.seconds >= 60: time_ago = f"{delta.seconds // 60}m ago" else: time_ago = "just now" alerts_data.append({ "alert": alert, "time_ago": time_ago, }) return templates.TemplateResponse("partials/alerts/alert_dropdown.html", { "request": request, "alerts": alerts_data, "total_count": alert_service.get_active_alert_count(), }) @router.get("/partials/list", response_class=HTMLResponse) async def get_alert_list( request: Request, db: Session = Depends(get_db), status: Optional[str] = Query(None), limit: int = Query(20), ): """ Return HTML partial for alert list page. """ alert_service = get_alert_service(db) if status: alerts = alert_service.get_all_alerts(status=status, limit=limit) else: alerts = alert_service.get_all_alerts(limit=limit) # Calculate relative time for each alert now = datetime.utcnow() alerts_data = [] for alert in alerts: delta = now - alert.created_at if delta.days > 0: time_ago = f"{delta.days}d ago" elif delta.seconds >= 3600: time_ago = f"{delta.seconds // 3600}h ago" elif delta.seconds >= 60: time_ago = f"{delta.seconds // 60}m ago" else: time_ago = "just now" alerts_data.append({ "alert": alert, "time_ago": time_ago, }) return templates.TemplateResponse("partials/alerts/alert_list.html", { "request": request, "alerts": alerts_data, "status_filter": status, }) # ============================================================================ # Cleanup # ============================================================================ @router.post("/cleanup-expired") async def cleanup_expired_alerts(db: Session = Depends(get_db)): """ Cleanup expired alerts (admin/maintenance endpoint). """ alert_service = get_alert_service(db) count = alert_service.cleanup_expired_alerts() return { "success": True, "cleaned_up": count, }