Files
smart-support/backend/app/analytics/api.py
Yaojia Wang af53111928 refactor: fix architectural issues across frontend and backend
Address all architecture review findings:

P0 fixes:
- Add API key authentication for admin endpoints (analytics, replay, openapi)
  and WebSocket connections via ADMIN_API_KEY env var
- Add PostgreSQL-backed PgSessionManager and PgInterruptManager for
  multi-worker production deployments (in-memory defaults preserved)

P1 fixes:
- Implement actual tool generation in OpenAPI approve_job endpoint
  using generate_tool_code() and generate_agent_yaml()
- Add missing clarification, interrupt_expired, and tool_result message
  handlers in frontend ChatPage

P2 fixes:
- Replace monkey-patching on CompiledStateGraph with typed GraphContext
- Replace 9-param dispatch_message with WebSocketContext dataclass
- Extract duplicate _envelope() into shared app/api_utils.py
- Replace mutable module-level counter with crypto.randomUUID()
- Remove hardcoded mock data from ReviewPage, use api.ts wrappers
- Remove `as any` type escape from ReplayPage

All 516 tests passing, 0 TypeScript errors.
2026-04-06 15:59:14 +02:00

61 lines
1.7 KiB
Python

"""Analytics API router -- dashboard metrics endpoint."""
from __future__ import annotations
import re
from dataclasses import asdict
from typing import TYPE_CHECKING
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from app.analytics.queries import get_analytics
from app.api_utils import envelope
from app.auth import require_admin_api_key
if TYPE_CHECKING:
from psycopg_pool import AsyncConnectionPool
router = APIRouter(
prefix="/api/analytics",
tags=["analytics"],
dependencies=[Depends(require_admin_api_key)],
)
_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 _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: '<N>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))