Serve the panel snapshot behind a reusable service-token dependency (task ae7e90ae)

The panel plane (D-012) needs an authenticated read surface the HA
integration polls: GET /api/v1/panel/snapshot. The token check lives in
ONE reusable dependency (api/dependencies.require_panel_token) — missing,
malformed, wrong, and empty-configured credentials all fail closed with
the same 401 (constant-time compare, uniform detail) — and the panel
router attaches it once so every later panel route inherits the check.

ApiConfig gains the required NESTQUEST_PANEL_TOKEN (same fail-fast
discipline as NESTQUEST_DB_PATH: unset/empty is a ConfigError, never a
silent default), create_app stores the resolved config on app.state so
the dependency reads it without module state, and the snapshot handler
stays a thin adapter: it calls the pure core build_snapshot with the
live database and one timezone-aware clock read, shapes each child's
instances through core.instance_payload(include_missed=False) so missed
instances are omitted from the panel payload (D-009), and returns the
shape documented by the PanelSnapshotResponse models in OpenAPI.

Tests (tests/test_api_panel.py) seed the household through the SAME
nestquest_core.* copy the app uses, pin the route's single clock read,
and assert 401 for missing/wrong/malformed tokens, 200 with the full
documented payload (children in sort order, counts, presence,
cycle_day), and that a past-due open (missed) instance never reaches
the payload.
This commit is contained in:
2026-09-22 22:16:27 -04:00
parent ca477125d6
commit db07670f44
7 changed files with 880 additions and 25 deletions
+11
View File
@@ -8,6 +8,10 @@ exposes the two endpoints every later task depends on:
app has imported core, opened the database, and applied migrations;
a cheap ``SELECT 1`` confirms the connection is live without leaking
the filesystem path on an unauthenticated route.
- ``GET /api/v1/panel/snapshot`` — the panel plane's today snapshot
(service-token protected; see :mod:`api.routes_panel`, whose router
carries the shared :func:`api.dependencies.require_panel_token`
check).
- ``GET /docs`` — FastAPI's auto-served OpenAPI document (Swagger UI);
``/openapi.json`` is the raw schema. No extra wiring is needed;
FastAPI serves both by default.
@@ -35,6 +39,7 @@ from fastapi import FastAPI
from api.config import ApiConfig
from api.database import DatabaseState, make_database
from api.routes_panel import router as panel_router
def _build_lifespan(db_path: str):
@@ -79,6 +84,12 @@ def create_app(config: ApiConfig | None = None) -> FastAPI:
lifespan=_build_lifespan(cfg.db_path),
)
# The resolved config rides on app.state so the reusable route
# dependencies (the panel service-token check) read the configured
# values from the app rather than module state.
app.state.config = cfg
app.include_router(panel_router)
@app.get("/health", summary="Service health")
async def health() -> dict[str, str]:
"""Return service health.