feat: database stats on the dashboard via GET /api/stats
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 23s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 14s
Documentation / Validate Documentation (push) Failing after 8s
Documentation / Generate API Documentation (push) Successful in 9s
Documentation / Build Sphinx Documentation (push) Successful in 17s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 3s

New SQLAdapter.get_database_stats() aggregates totals, station count,
date range, and hourly-slot coverage in one query per dialect. The
endpoint follows the existing 503-guard/to_thread/TTL-cache pattern;
the dashboard gains a five-tile stats strip on the existing refresh
cadence. Coverage denominator is hour-truncated so off-hour endpoints
cannot push it past 100%; MySQL slot expression avoids % characters
that would break under pyformat bind interpolation.
This commit is contained in:
2026-08-10 23:27:36 +07:00
parent 5e62ea529d
commit 7befc82ff5
3 changed files with 140 additions and 0 deletions
+43
View File
@@ -51,6 +51,10 @@ FORECAST_CACHE: Dict[str, tuple] = {}
FORECAST_CACHE_LOCK = Lock()
FORECAST_TTL = 900 # 15 minutes
DB_STATS_CACHE: Dict[str, tuple] = {}
DB_STATS_CACHE_LOCK = Lock()
DB_STATS_TTL = 300 # 5 minutes
# Admin API protection. Read/dashboard endpoints stay public; anything that
# mutates state or leaks configuration requires the X-API-Key header matching
# ADMIN_API_KEY. Secure by default: with no key configured, those endpoints
@@ -630,6 +634,45 @@ async def get_station_measurements(
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/stats")
async def get_database_stats():
"""Get database coverage statistics (totals, date range, hourly coverage)"""
increment_counter("api_requests", labels={"endpoint": "api_stats"})
scraper = app_state["scraper"]
if not scraper or not scraper.db_adapter:
raise HTTPException(status_code=503, detail="Database not available")
now = time.monotonic()
with DB_STATS_CACHE_LOCK:
cached = DB_STATS_CACHE.get("all")
if cached and now - cached[0] < DB_STATS_TTL:
return cached[1]
try:
stats = await asyncio.to_thread(scraper.db_adapter.get_database_stats)
except Exception as e:
logger.error(f"Error fetching database stats: {e}")
raise HTTPException(status_code=500, detail=str(e))
if stats is None:
raise HTTPException(status_code=503, detail="Database statistics unavailable")
first_ts = stats["first_timestamp"]
last_ts = stats["last_timestamp"]
data = {
"total_measurements": stats["total_measurements"],
"station_count": stats["station_count"],
"first_timestamp": first_ts.isoformat(),
"last_timestamp": last_ts.isoformat(),
"days_spanned": (last_ts.date() - first_ts.date()).days + 1,
"coverage_percent": stats["coverage_percent"],
}
with DB_STATS_CACHE_LOCK:
DB_STATS_CACHE["all"] = (now, data)
return data
@app.post("/scrape/trigger", dependencies=[Depends(require_admin_key)])
async def trigger_scraping(background_tasks: BackgroundTasks):
"""Trigger manual data scraping"""