The backend image now builds the SPA and serves it on the API's own origin (fluksio/spa.py), so an instance is a single container with no proxy, DNS, port 80 or .env: `docker compose up -d` with the new compose.yml runs it on localhost:8080. It starts on fluksio serve (persisted secret key, first admin printed once) and brings its own Redis when REDIS_HOST is unset. Slimmer: python:3.13-slim, no dev group, uv once, fastapi without the cloud CLI. Optional features are per-feature extras (mqtt, influx, email, sentry, ml, server for all) picked with the EXTRAS build argument; numpy moved to `ml`. ffmpeg left the image for imageio-ffmpeg via the Modules screen. The operator stack runs the same image with app. and api. routed to it; the nginx frontend service, the LAN nginx proxy and DOMAIN in the backend are gone, and the MCP issuer is FRONTEND_HOST.
40 lines
1.4 KiB
Python
40 lines
1.4 KiB
Python
import base64
|
|
import hashlib
|
|
from pathlib import Path
|
|
|
|
from starlette.applications import Starlette
|
|
from starlette.responses import PlainTextResponse
|
|
from starlette.routing import Route
|
|
from starlette.testclient import TestClient
|
|
|
|
from fluksio.spa import Dashboard
|
|
|
|
THEME = "document.documentElement.classList.add('dark')"
|
|
|
|
|
|
def _client(dist: Path) -> TestClient:
|
|
(dist / "assets").mkdir(parents=True)
|
|
(dist / "index.html").write_text(f"<html><script>{THEME}</script></html>")
|
|
(dist / "assets" / "app-1a2b.js").write_text("console.log(1)")
|
|
api = Starlette(routes=[Route("/api/v1/ping", lambda _: PlainTextResponse("api"))])
|
|
return TestClient(Dashboard(api, dist))
|
|
|
|
|
|
def test_the_dashboard_and_the_api_share_one_origin(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
|
|
# A client-side route survives a reload, with the inline script admitted.
|
|
page = client.get("/flows/house")
|
|
digest = base64.b64encode(hashlib.sha256(THEME.encode()).digest()).decode()
|
|
assert THEME in page.text
|
|
assert page.headers["cache-control"] == "no-cache"
|
|
assert f"'sha256-{digest}'" in page.headers["content-security-policy"]
|
|
|
|
asset = client.get("/assets/app-1a2b.js")
|
|
assert asset.text == "console.log(1)"
|
|
assert "immutable" in asset.headers["cache-control"]
|
|
|
|
assert client.get("/api/v1/ping").text == "api"
|
|
# Nothing outside the build directory is served, only the shell.
|
|
assert THEME in client.get("/assets/../../etc/passwd").text
|