1919from pathlib import Path
2020from typing import Any , Optional , Literal
2121
22+ from contextlib import asynccontextmanager
23+
2224from pydantic import field_validator
2325
2426from fastapi import FastAPI , File , Form , HTTPException , UploadFile , Header , Query , Depends , Request
4042 get_subscriptions_for_organization ,
4143 create_organization_alert ,
4244 get_alerts_for_organization ,
45+ get_pending_alerts ,
4346 acknowledge_alert ,
4447 mark_alert_delivered ,
4548)
4851from climatevision .api .auth import require_api_key
4952from climatevision .governance import explain_prediction , SHAPExplainer
5053from climatevision .security .api_security import SecurityMiddleware
54+ from climatevision .workers .alert_delivery import AlertDeliveryWorker
5155
5256logger = logging .getLogger (__name__ )
5357
@@ -381,6 +385,18 @@ async def dispatch(self, request: Request, call_next: Any) -> Response:
381385def create_app () -> FastAPI :
382386 init_db ()
383387
388+ # Set up alert delivery worker
389+ alert_worker = AlertDeliveryWorker ()
390+
391+ @asynccontextmanager
392+ async def lifespan (_app : FastAPI ):
393+ """Start background workers on app startup, stop on shutdown."""
394+ await alert_worker .start ()
395+ logger .info ("Alert delivery worker started" )
396+ yield
397+ await alert_worker .stop ()
398+ logger .info ("Alert delivery worker stopped" )
399+
384400 app = FastAPI (
385401 title = "ClimateVision API" ,
386402 version = "0.2.0" ,
@@ -396,6 +412,7 @@ def create_app() -> FastAPI:
396412 docs_url = "/docs" ,
397413 redoc_url = "/redoc" ,
398414 openapi_url = "/openapi.json" ,
415+ lifespan = lifespan ,
399416 )
400417
401418 app .add_middleware (AuditLogMiddleware )
@@ -1143,6 +1160,39 @@ def mark_alert_as_delivered(
11431160 raise HTTPException (status_code = 404 , detail = "Alert not found" )
11441161 return {"success" : True , "alert_id" : alert_id }
11451162
1163+ @app .get ("/api/organizations/{org_id}/alerts/pending" )
1164+ def list_pending_alerts (
1165+ org_id : int ,
1166+ limit : int = Query (default = 50 , le = 200 ),
1167+ ) -> list [AlertResponse ]:
1168+ """List pending (undelivered) alerts for an organization.
1169+
1170+ This endpoint is designed for monitoring and dashboard use,
1171+ showing alerts that the delivery worker has not yet processed.
1172+ """
1173+ org = get_organization (org_id )
1174+ if not org :
1175+ raise HTTPException (status_code = 404 , detail = "Organization not found" )
1176+
1177+ alerts = get_pending_alerts (max_attempts = 3 , limit = limit )
1178+ # Filter to this specific organization's alerts
1179+ org_alerts = [a for a in alerts if a ["organization_id" ] == org_id ]
1180+
1181+ return [
1182+ AlertResponse (
1183+ id = alert ["id" ],
1184+ organization_id = alert ["organization_id" ],
1185+ alert_type = alert ["alert_type" ],
1186+ severity = alert ["severity" ],
1187+ title = alert ["title" ],
1188+ message = alert ["message" ],
1189+ delivered = bool (alert ["delivered" ]),
1190+ acknowledged = bool (alert ["acknowledged" ]),
1191+ created_at = alert ["created_at" ],
1192+ )
1193+ for alert in org_alerts [:limit ]
1194+ ]
1195+
11461196 # ===== Static Files =====
11471197
11481198 # Serve built frontend when dist exists (production mode)
0 commit comments