From 6009b4854acc641b85b9d078b20d2fc8e936b16e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 25 Apr 2026 02:45:36 +0100 Subject: [PATCH] Version GUI assets from index route --- src/repixelizer/gui.py | 19 ++++++++++++++++++- tests/test_gui.py | 12 ++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/repixelizer/gui.py b/src/repixelizer/gui.py index 61cc3ae..ac9d243 100644 --- a/src/repixelizer/gui.py +++ b/src/repixelizer/gui.py @@ -357,6 +357,18 @@ def _static_dir() -> Path: return Path(__file__).with_name("gui_static") +def _versioned_gui_index(static_dir: Path) -> str: + index_path = static_dir / "index.html" + styles_path = static_dir / "styles.css" + script_path = static_dir / "app.js" + styles_version = int(styles_path.stat().st_mtime_ns) + script_version = int(script_path.stat().st_mtime_ns) + html = index_path.read_text(encoding="utf-8") + html = html.replace("./styles.css", f"./styles.css?v={styles_version}") + html = html.replace("./app.js", f"./app.js?v={script_version}") + return html + + def create_app(): deps = _require_gui_dependencies() FastAPI = deps["FastAPI"] @@ -459,7 +471,12 @@ def create_app(): return StreamingResponse(generate(), media_type="text/event-stream") if static_dir.exists(): - app.mount("/app", StaticFiles(directory=static_dir, html=True), name="app") + @app.get("/app") + @app.get("/app/") + def gui_index(): + return HTMLResponse(_versioned_gui_index(static_dir)) + + app.mount("/app", StaticFiles(directory=static_dir, html=False), name="app") @app.get("/") def root(): diff --git a/tests/test_gui.py b/tests/test_gui.py index e71ead5..da54025 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -17,7 +17,7 @@ from repixelizer.params import SolverHyperParams from repixelizer.types import InferenceCandidate, InferenceResult, PhaseFieldSourceAnalysis, SolverArtifacts -async def _get_response_headers(app, path: str) -> tuple[int, dict[str, str]]: +async def _get_response(app, path: str) -> tuple[int, dict[str, str], bytes]: messages: list[dict[str, object]] = [] async def receive(): @@ -42,12 +42,13 @@ async def _get_response_headers(app, path: str) -> tuple[int, dict[str, str]]: } await app(scope, receive, send) start = next(message for message in messages if message["type"] == "http.response.start") + body = b"".join(message.get("body", b"") for message in messages if message["type"] == "http.response.body") status = int(start["status"]) headers = { key.decode("latin-1").lower(): value.decode("latin-1") for key, value in start.get("headers", []) } - return status, headers + return status, headers, body def test_run_pipeline_rgba_emits_observer_events_for_gui() -> None: @@ -126,8 +127,8 @@ def test_gui_static_assets_disable_browser_caching() -> None: from repixelizer.gui import create_app app = create_app() - html_status, html_headers = asyncio.run(_get_response_headers(app, "/app/")) - js_status, js_headers = asyncio.run(_get_response_headers(app, "/app/app.js")) + html_status, html_headers, html_body = asyncio.run(_get_response(app, "/app/")) + js_status, js_headers, _js_body = asyncio.run(_get_response(app, "/app/app.js")) assert html_status == 200 assert js_status == 200 @@ -135,6 +136,9 @@ def test_gui_static_assets_disable_browser_caching() -> None: assert js_headers["cache-control"] == "no-store, no-cache, must-revalidate, max-age=0" assert html_headers["pragma"] == "no-cache" assert js_headers["pragma"] == "no-cache" + html_text = html_body.decode("utf-8") + assert "./styles.css?v=" in html_text + assert "./app.js?v=" in html_text def test_infer_lattice_emits_search_progress_events(monkeypatch) -> None: