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
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:
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user