"""ESPresso Frame server: pulls photos from Immich, pre-processes them for the panel, and serves ESP32 frames ready-to-display images. This module is assembly only -- routes live in app/routers/ (device.py for the firmware-facing /frame/* protocol, api.py for the web UI's /api/*), storage in SQLite via models.py/db.py, with migration.py importing a pre-database config.json deployment on first boot.""" from __future__ import annotations import logging from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from . import migration from .auth import MANAGEMENT_TOKEN_COOKIE, browser_token_valid, management_token from .db import SessionLocal from .quiet_hours import ALL_TIMEZONES from .routers import api, device from .routers.common import default_frame, immich_creds logger = logging.getLogger(__name__) # Schema + legacy-config import, before the first request is served. migration.run_migrations() app = FastAPI(title="ESPresso Frame Server") templates = Jinja2Templates(directory="app/templates") app.include_router(device.router) app.include_router(api.router) @app.get("/health") def health() -> dict: return {"status": "ok"} @app.get("/", response_class=HTMLResponse) def index(request: Request): """The web UI (Phase A: still the single-frame page, bound to the default frame). Handles the unauthorized case itself with a friendly token prompt rather than a bare 401, since this is the one route a human lands on with no token yet.""" if not browser_token_valid(request): supplied = request.query_params.get("token") return templates.TemplateResponse( "token_prompt.html", {"request": request, "wrong": supplied is not None} ) with SessionLocal() as db: frame = default_frame(db) immich_url, _ = immich_creds(frame) response = templates.TemplateResponse( "index.html", { "request": request, "cfg": frame, "immich_url": immich_url, "timezones": ALL_TIMEZONES, }, ) supplied = request.query_params.get("token") if management_token() and supplied == management_token(): # Query-param access (typically the manage-menu QR code) earns a # cookie so the rest of this visit's fetch()/ calls -- which # never carry the query string -- stay authorized too. response.set_cookie( MANAGEMENT_TOKEN_COOKIE, supplied, max_age=86400 * 365, httponly=True, samesite="lax" ) return response