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
@@ -387,6 +391,18 @@ async def dispatch(self, request: Request, call_next: Any) -> Response:
387391def create_app () -> FastAPI :
388392 init_db ()
389393
394+ # Set up alert delivery worker
395+ alert_worker = AlertDeliveryWorker ()
396+
397+ @asynccontextmanager
398+ async def lifespan (_app : FastAPI ):
399+ """Start background workers on app startup, stop on shutdown."""
400+ await alert_worker .start ()
401+ logger .info ("Alert delivery worker started" )
402+ yield
403+ await alert_worker .stop ()
404+ logger .info ("Alert delivery worker stopped" )
405+
390406 app = FastAPI (
391407 title = "ClimateVision API" ,
392408 version = "0.2.0" ,
@@ -402,6 +418,7 @@ def create_app() -> FastAPI:
402418 docs_url = "/docs" ,
403419 redoc_url = "/redoc" ,
404420 openapi_url = "/openapi.json" ,
421+ lifespan = lifespan ,
405422 )
406423
407424 app .add_middleware (AuditLogMiddleware )
@@ -1146,6 +1163,39 @@ def mark_alert_as_delivered(
11461163 raise HTTPException (status_code = 404 , detail = "Alert not found" )
11471164 return {"success" : True , "alert_id" : alert_id }
11481165
1166+ @app .get ("/api/organizations/{org_id}/alerts/pending" )
1167+ def list_pending_alerts (
1168+ org_id : int ,
1169+ limit : int = Query (default = 50 , le = 200 ),
1170+ ) -> list [AlertResponse ]:
1171+ """List pending (undelivered) alerts for an organization.
1172+
1173+ This endpoint is designed for monitoring and dashboard use,
1174+ showing alerts that the delivery worker has not yet processed.
1175+ """
1176+ org = get_organization (org_id )
1177+ if not org :
1178+ raise HTTPException (status_code = 404 , detail = "Organization not found" )
1179+
1180+ alerts = get_pending_alerts (max_attempts = 3 , limit = limit )
1181+ # Filter to this specific organization's alerts
1182+ org_alerts = [a for a in alerts if a ["organization_id" ] == org_id ]
1183+
1184+ return [
1185+ AlertResponse (
1186+ id = alert ["id" ],
1187+ organization_id = alert ["organization_id" ],
1188+ alert_type = alert ["alert_type" ],
1189+ severity = alert ["severity" ],
1190+ title = alert ["title" ],
1191+ message = alert ["message" ],
1192+ delivered = bool (alert ["delivered" ]),
1193+ acknowledged = bool (alert ["acknowledged" ]),
1194+ created_at = alert ["created_at" ],
1195+ )
1196+ for alert in org_alerts [:limit ]
1197+ ]
1198+
11491199 # ===== Static Files =====
11501200
11511201 # Serve built frontend when dist exists (production mode)
0 commit comments