--- description: dashboard /api routes, tool-runner, metrics middleware, health routes, HAProxy edge, mount order paths: - "nike/server.py" --- # FastAPI dashboard, routing & edge - **Mount order matters and is load-bearing.** `dashboard.mount("/mcp", _mcp_app)` goes **before** the SPA `StaticFiles` mount at `/` (which is greedy with `html=True` SPA fallback). MCP must be mounted first or `/` swallows `/mcp`. The MCP ASGI app is built early (`mcp.http_app(path="/")`) so its lifespan can be nested inside the FastAPI lifespan — keep that wiring. - **`/api/*` routes are thin façades** over `db`, `sportsdb`, and `mcp`. `/api/status` (health cards), `/api/tools` (derived from `mcp.list_tools()` — the single source the dashboard reads), `/api/logs` (the in-memory request log), `/api/cache/invalidate` (flush both caches), `/api/run` (tool-runner: `mcp.get_tool` → `tool.fn(**args)`), `/api/v1/telemetry` (browser error sink). Keep `/api/tools` derived, never hand-maintained. - **Prometheus middleware skips health/MCP paths.** `_SKIP_METRICS_PREFIXES = ("/live","/ready","/metrics","/mcp")` — probes and MCP transport must not inflate `nike_http_requests_total`. It also normalises UUIDs in paths to `{id}` to bound label cardinality. Keep both behaviours; add new health-ish prefixes to the skip list. - **`/metrics` is Prometheus exposition, IP-gated in Python.** `_METRICS_ALLOWED_NETS` = the standard four ranges (`10.10.0.0/24`, `172.16.0.0/12`, `127.0.0.0/8`, `::1/128`), no auth — HAProxy/Prometheus can't authenticate (`red_panda_standards.md`). Don't widen it or add app-side auth. - **Health routes register both slash forms** (`/live` + `/live/`, `/ready` + `/ready/`). `/ready` returns 503 when `db.check_connection()` reports down. `/live` is a bare `{"status": "ok"}`. - **HAProxy is the edge.** `uvicorn.run(..., proxy_headers=True, forwarded_allow_ips=config.TRUSTED_PROXY_IPS, ws="wsproto")`. The write routes (`/api/run`, `/api/cache/invalidate`) are **not individually authenticated** — access control is at HAProxy; the code comment says so. If you add a write route, keep that assumption explicit; if Nike could be exposed directly, it needs auth. `/api/v1/telemetry` stays unprotected on purpose (`sendBeacon` can't set headers). - **Structured logging is re-applied in the lifespan** (`configure_logging` after `create_pool`) because uvicorn overwrites handlers on `config.load()`. Don't drop the second call, or JSON logging silently reverts to uvicorn's default.