"""Analytics API router -- dashboard metrics endpoint.""" from __future__ import annotations import re from dataclasses import asdict from typing import TYPE_CHECKING, Any from fastapi import APIRouter, HTTPException, Query, Request from app.analytics.queries import get_analytics if TYPE_CHECKING: from psycopg_pool import AsyncConnectionPool router = APIRouter(prefix="/api/analytics", tags=["analytics"]) _RANGE_PATTERN = re.compile(r"^(\d+)d$") _DEFAULT_RANGE = "7d" _MAX_RANGE_DAYS = 365 async def _get_pool(request: Request) -> AsyncConnectionPool: """Dependency: extract the shared pool from app state.""" return request.app.state.pool def _envelope(data: Any, *, success: bool = True, error: str | None = None) -> dict: return {"success": success, "data": data, "error": error} def _parse_range(range_str: str) -> int: """Parse 'Xd' range string to integer days. Raises 400 on invalid format.""" match = _RANGE_PATTERN.match(range_str) if not match: raise HTTPException( status_code=400, detail="Invalid range format. Expected: 'd' e.g. '7d', '30d'.", ) days = int(match.group(1)) if days < 1 or days > _MAX_RANGE_DAYS: raise HTTPException( status_code=400, detail=f"Range must be between 1 and {_MAX_RANGE_DAYS} days.", ) return days @router.get("") async def analytics( request: Request, range: str = Query(default=_DEFAULT_RANGE, alias="range"), # noqa: A002 ) -> dict: """Return aggregated analytics metrics for the given time range.""" range_days = _parse_range(range) pool = await _get_pool(request) result = await get_analytics(pool, range_days=range_days) return _envelope(asdict(result))