[verified] feat: add live river dashboard

Add mapped river and ThaiWater sensor layers, PostgreSQL history charts, API endpoints, and dashboard tests.
This commit is contained in:
2026-08-09 16:59:25 +07:00
parent e5936d5717
commit ae5d0a13d7
11 changed files with 800 additions and 42 deletions
+50 -1
View File
@@ -9,14 +9,17 @@ from contextlib import asynccontextmanager
from datetime import datetime, timedelta
from typing import Any, Dict, List
from fastapi import BackgroundTasks, FastAPI, HTTPException
import requests
from fastapi import BackgroundTasks, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from .config import Config
from .health_check import APIHealthCheck, DatabaseHealthCheck, HealthCheckManager, MemoryHealthCheck
from .logging_config import get_logger, setup_logging
from .metrics import get_metrics_collector, increment_counter, set_gauge
from .postgres_history import PostgresHistory
from .schemas import (
HealthResponse,
MeasurementResponse,
@@ -26,6 +29,7 @@ from .schemas import (
StationResponse,
StationUpdateModel,
)
from .thaiwater import ThaiWaterClient
from .water_scraper_v3 import EnhancedWaterMonitorScraper
logger = get_logger(__name__)
@@ -108,6 +112,7 @@ app = FastAPI(
version="3.1.3",
lifespan=lifespan,
)
app.mount("/static", StaticFiles(directory=os.path.dirname(_DASHBOARD_HTML_PATH)), name="static")
# Add CORS middleware.
# Origins come from CORS_ALLOW_ORIGINS (comma-separated). When none are configured
@@ -419,6 +424,50 @@ def _to_measurement_response(measurement: Dict[str, Any]) -> MeasurementResponse
)
@app.get("/sensors/thaiwater")
async def get_thaiwater_sensors():
"""Get current ThaiWater water-level sensors in the Ping basin."""
increment_counter("api_requests", labels={"endpoint": "thaiwater_sensors"})
try:
client = ThaiWaterClient(
api_key=Config.THAIWATER_API_KEY,
timeout=Config.REQUEST_TIMEOUT,
)
return await asyncio.to_thread(client.fetch_ping_sensors)
except RuntimeError as error:
raise HTTPException(status_code=503, detail=str(error))
except requests.RequestException as error:
logger.error(f"Error fetching ThaiWater sensors: {error}")
raise HTTPException(status_code=502, detail="ThaiWater API unavailable")
@app.get("/measurements/history/{station_code}")
async def get_postgres_history(
station_code: str,
hours: int = Query(168, ge=1, le=24 * 365),
limit: int = Query(2000, ge=1, le=5000),
):
"""Get historical measurements for a station from PostgreSQL."""
try:
db_config = Config.get_database_config()
if db_config["type"] != "postgresql":
raise RuntimeError("PostgreSQL is not configured")
history = PostgresHistory(db_config["connection_string"])
end_time = datetime.now()
return await asyncio.to_thread(
history.station_history,
station_code,
end_time - timedelta(hours=hours),
end_time,
limit,
)
except RuntimeError as error:
raise HTTPException(status_code=503, detail=str(error))
except Exception as error:
logger.error(f"Error fetching PostgreSQL history: {error}")
raise HTTPException(status_code=502, detail="PostgreSQL history unavailable")
@app.get("/measurements/latest", response_model=List[MeasurementResponse])
async def get_latest_measurements(limit: int = 100):
"""Get latest measurements from all stations"""