diff --git a/backend/druks/api/server.py b/backend/druks/api/server.py index 1a464f4b..7150811b 100644 --- a/backend/druks/api/server.py +++ b/backend/druks/api/server.py @@ -376,11 +376,12 @@ async def mcp_not_found(path: str) -> None: def serve_spa(app: FastAPI, dist: Path = _SPA_DIST) -> None: - """Serve the platform's own dashboard the way an app's ``dist/`` is - served — FastAPI's low-priority frontend routes, so every API path (and every - app frontend, registered earlier) wins, and unknown paths fall back to - index.html for client-side routing. A bare pip install ships no SPA build; - then the API serves JSON only.""" + """Serve the platform's own dashboard the way Druks serves an app's ``dist/``. + FastAPI serves both from low-priority frontend routes. Every API path wins over + them. Every app frontend registers earlier and also wins. An unknown path falls + back to index.html for client-side routing. The index.html fallback needs a + navigation request with an ``Accept: text/html`` header. A bare pip install + ships no SPA build. Without a build, the API serves JSON only.""" if (dist / "index.html").is_file(): app.frontend("/", directory=dist) diff --git a/backend/tests/test_spa_serving.py b/backend/tests/test_spa_serving.py index 02b97294..ea508f32 100644 --- a/backend/tests/test_spa_serving.py +++ b/backend/tests/test_spa_serving.py @@ -5,6 +5,7 @@ from druks.db import db_session from fastapi import Depends, FastAPI, HTTPException from fastapi.testclient import TestClient +from sqlalchemy.orm import Session def _app_with_spa(dist: Path) -> FastAPI: @@ -30,7 +31,7 @@ def test_spa_serves_index_assets_and_client_routes(tmp_path): assert "druks" in client.get("/").text assert client.get("/assets/app.js").status_code == 200 # An unknown path is a client-side route: index.html, not a 404. - assert "druks" in client.get("/work_items/42").text + assert "druks" in client.get("/work_items/42", headers={"Accept": "text/html"}).text def test_api_paths_win_over_the_spa(tmp_path): @@ -55,15 +56,16 @@ def test_cache_policy_lives_with_the_server(tmp_path): assert asset.headers["cache-control"] == "public, max-age=31536000, immutable" # index.html must revalidate every load or a deploy leaves stale bundles. assert client.get("/").headers["cache-control"] == "no-cache" - assert client.get("/work_items/42").headers["cache-control"] == "no-cache" + assert ( + client.get("/work_items/42", headers={"Accept": "text/html"}).headers["cache-control"] + == "no-cache" + ) # API responses carry no frontend cache policy; nor does a missing asset. assert "cache-control" not in client.get("/api/thing").headers assert "cache-control" not in client.get("/assets/gone.js").headers def test_request_that_never_touches_the_db_opens_no_connection(monkeypatch): - from sqlalchemy.orm import Session - connected = [] original = Session._connection_for_bind