"""The dashboard, served by the API on its own origin. One process and one port: the built SPA comes off disk, and every path the API does not own answers with ``index.html``, so a client-side route survives a reload. Same origin also means the dashboard calls the API as a path, needs no CORS allowance and no second hostname, which is what lets the whole instance sit behind a single published port. """ from __future__ import annotations import base64 import hashlib import mimetypes import re from pathlib import Path from starlette.datastructures import Headers from starlette.responses import FileResponse from starlette.types import ASGIApp, Receive, Scope, Send #: What the API answers itself. Everything else is the dashboard's. API_PREFIXES = ("/api/", "/hooks/", "/mcp", "/.well-known/", "/docs", "/redoc") # Older mime tables predate the manifest, and it would go out as a download. mimetypes.add_type("application/manifest+json", ".webmanifest") def _accepts_gzip(header: str) -> bool: """An explicit refusal overrides a wildcard, including on a cached asset.""" qualities: dict[str, float] = {} for entry in header.lower().split(","): encoding, *parameters = entry.strip().split(";") quality = 1.0 for parameter in parameters: key, _, value = parameter.strip().partition("=") if key == "q": try: quality = float(value) except ValueError: quality = 0.0 qualities[encoding.strip()] = quality if 0.0 <= quality <= 1.0 else 0.0 gzip = qualities.get("gzip", qualities.get("*", 0.0)) return gzip > 0 and gzip >= qualities.get("identity", 0.0) def content_security_policy(index_html: str) -> str: """The policy for the dashboard, admitting its inline scripts by hash. The hash is taken from the file being served, so editing the theme script in ``index.html`` cannot leave a stale one behind. style-src 'unsafe-inline': CodeMirror, Radix's scroll lock and the toasts insert