"""Space entrypoint: physix.server.app:app + static UI mount. Imported at runtime by the Dockerfile's CMD via ``uvicorn _space_app:app``. What this wrapper adds on top of ``physix.server.app:app``: 1. ``GET /`` -> 302 to ``/web/`` (so the bare Space URL doesn't 404 — OpenEnv's ``create_fastapi_app`` does NOT add a root redirect; that's only in the higher-level wrapper which mounts Gradio at ``/web`` and would clobber our React SPA). 2. ``GET /web`` -> 302 to ``/web/`` (same reason; users hit the no-trailing-slash variant from outside links). 3. ``StaticFiles`` mount at ``/web/`` serving the built Vite SPA. The vite build was run with ``--base=/web/`` so all asset URLs in the emitted ``index.html`` already include the prefix. Kept as a real .py file (not a heredoc inside the Dockerfile) so any syntax error is caught by the build's static analysis rather than at runtime — saved several deploy-fail loops in earlier iterations. """ from __future__ import annotations from pathlib import Path from fastapi.responses import RedirectResponse from fastapi.staticfiles import StaticFiles from physix.server.app import app _STATIC_DIR = Path("/app/static") @app.get("/", include_in_schema=False) async def _root_redirect() -> RedirectResponse: return RedirectResponse(url="/web/") @app.get("/web", include_in_schema=False) async def _web_no_slash_redirect() -> RedirectResponse: return RedirectResponse(url="/web/") if _STATIC_DIR.is_dir(): # html=True makes StaticFiles serve index.html for directory hits and # fall back to it for unknown sub-paths (so client-side React routing # works). Mounted last so registered API routes (/web/metadata, # /web/reset, /web/step from OpenEnv; /interactive/* from physix) # always win over the static handler. app.mount( "/web", StaticFiles(directory=str(_STATIC_DIR), html=True), name="ui", )