feat: add analysis pages and raster risk map
Ship a new app version with broader analytics, restructured dashboards, and a server-rendered risk map. Frontend: - Add Overview, Demographic, Disease, and Environmental Health analysis pages - Add AnomalyMarkers, CalendarHeatmap, and MetricHeatmapTable components - Rebuild Alerts map onto server-rendered raster risk tiles; expand Monitoring, Trend, and District Comparison views - Extend API client, stores, and TypeScript types Backend: - Add environment router (pollutants, lag correlations) - Add risk_raster util serving XYZ 100m risk tiles - Expand cases endpoints (demographics, seasonality, diagnoses) and insights; harden auth and file-based loaders Data & tooling: - Add processed outpatient/inpatient/combined case parquet (LFS) - Add nested CLAUDE.md guides, pyrightconfig, and test updates
This commit is contained in:
@@ -5,6 +5,8 @@ Generates alerts from high-risk grids in GeoJSON files
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from functools import lru_cache
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from config import DATA_DIR, ALERT_P1_RISK, ALERT_P2_RISK, WUHAN_BOUNDS, LAT_STEP, LON_STEP, MAX_ALERTS
|
||||
@@ -32,8 +34,13 @@ def grid_id_to_center(grid_id: str) -> tuple[float, float]:
|
||||
return lat, lon
|
||||
|
||||
|
||||
def generate_alerts_for_date(date: str) -> List[Alert]:
|
||||
"""Generate alerts for high-risk grids on a specific date.
|
||||
@lru_cache(maxsize=8)
|
||||
def _generate_alerts_cached(date: str) -> List[Alert]:
|
||||
"""Heavy synchronous worker: parse the ~45MB GeoJSON and build alerts.
|
||||
|
||||
Cached by date so the file is parsed once per date. This runs blocking
|
||||
json.load + per-feature loops, so callers must invoke it off the event
|
||||
loop (see generate_alerts_for_date).
|
||||
|
||||
Phase 1: iterate features, aggregate max risk per 100m grid cell.
|
||||
Phase 2: build Alert objects from aggregated grid cells.
|
||||
@@ -111,6 +118,16 @@ def generate_alerts_for_date(date: str) -> List[Alert]:
|
||||
return alerts[:MAX_ALERTS]
|
||||
|
||||
|
||||
async def generate_alerts_for_date(date: str) -> List[Alert]:
|
||||
"""Async accessor: run the cached heavy parser in a thread pool.
|
||||
|
||||
Offloading the blocking json.load + per-feature aggregation keeps the
|
||||
event loop free. The lru_cache lives on the worker, so warm dates return
|
||||
near-instantly without re-parsing.
|
||||
"""
|
||||
return await asyncio.to_thread(_generate_alerts_cached, date)
|
||||
|
||||
|
||||
@router.get("", response_model=AlertResponse)
|
||||
async def list_alerts(date: str | None = None, priority: str | None = None, min_risk: float | None = None):
|
||||
if date is not None and not validate_date_format(date):
|
||||
@@ -118,7 +135,7 @@ async def list_alerts(date: str | None = None, priority: str | None = None, min_
|
||||
if date is None:
|
||||
date = get_latest_date()
|
||||
|
||||
alerts = generate_alerts_for_date(date)
|
||||
alerts = await generate_alerts_for_date(date)
|
||||
|
||||
if priority:
|
||||
alerts = [a for a in alerts if a.priority == priority]
|
||||
@@ -140,7 +157,7 @@ async def get_alert(alert_id: str, date: str | None = None):
|
||||
if date is None:
|
||||
date = get_latest_date()
|
||||
|
||||
alerts = generate_alerts_for_date(date)
|
||||
alerts = await generate_alerts_for_date(date)
|
||||
|
||||
for alert in alerts:
|
||||
if alert.alert_id == alert_id:
|
||||
@@ -156,7 +173,7 @@ async def get_p1_alerts(date: str | None = None):
|
||||
if date is None:
|
||||
date = get_latest_date()
|
||||
|
||||
alerts = generate_alerts_for_date(date)
|
||||
alerts = await generate_alerts_for_date(date)
|
||||
p1_alerts = [a for a in alerts if a.priority == "P1"]
|
||||
|
||||
return AlertResponse(
|
||||
@@ -173,7 +190,7 @@ async def get_p2_alerts(date: str | None = None):
|
||||
if date is None:
|
||||
date = get_latest_date()
|
||||
|
||||
alerts = generate_alerts_for_date(date)
|
||||
alerts = await generate_alerts_for_date(date)
|
||||
p2_alerts = [a for a in alerts if a.priority == "P2"]
|
||||
|
||||
return AlertResponse(
|
||||
@@ -190,7 +207,7 @@ async def get_grid_alerts(grid_id: str, date: str | None = None):
|
||||
if date is None:
|
||||
date = get_latest_date()
|
||||
|
||||
alerts = generate_alerts_for_date(date)
|
||||
alerts = await generate_alerts_for_date(date)
|
||||
grid_alerts = [a for a in alerts if a.grid_id == grid_id]
|
||||
|
||||
return AlertResponse(
|
||||
|
||||
Reference in New Issue
Block a user