Loading .dockerignore 0 → 100644 +10 −0 Changes for .dockerignore: 10 added lines, 0 removed lines. Original line number Diff line number Diff line * !pyproject.toml !uv.lock !README.md !src/portal_service/ !portal-gui/ **/__pycache__ portal-gui/node_modules portal-gui/dist portal-gui/.env*.local Dockerfile 0 → 100644 +25 −0 Changes for Dockerfile: 25 added lines, 0 removed lines. Original line number Diff line number Diff line FROM node:22-slim AS gui WORKDIR /gui COPY portal-gui/package.json portal-gui/package-lock.json ./ RUN npm ci --no-audit --no-fund COPY portal-gui/ ./ RUN npm run build FROM python:3.12-slim AS backend RUN pip install --no-cache-dir uv WORKDIR /app ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy COPY pyproject.toml uv.lock ./ RUN uv sync --locked --no-install-project COPY README.md ./ COPY src/ src/ RUN uv sync --locked --no-editable FROM python:3.12-slim WORKDIR /app COPY --from=backend /app/.venv .venv COPY --from=gui /gui/dist gui ENV PATH=/app/.venv/bin:$PATH GUI_DIR=/app/gui USER 65534 EXPOSE 8000 CMD ["uvicorn", "portal_service.main:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"] src/portal_service/api/gui.py 0 → 100644 +20 −0 Changes for src/portal_service/api/gui.py: 20 added lines, 0 removed lines. Original line number Diff line number Diff line from starlette.exceptions import HTTPException from starlette.responses import Response from starlette.staticfiles import StaticFiles from starlette.types import Scope class GuiStaticFiles(StaticFiles): """The built GUI. Unknown paths get index.html, so client-side routes survive a reload.""" async def get_response(self, path: str, scope: Scope) -> Response: try: response = await super().get_response(path, scope) except HTTPException as e: if e.status_code != 404 or path.startswith("api/"): raise response = await super().get_response("index.html", scope) # Keeps signed-in pages out of the back-forward cache, as the Vite server does. if response.media_type == "text/html": response.headers["Cache-Control"] = "no-store" return response src/portal_service/config.py +2 −0 Changes for src/portal_service/config.py: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -132,6 +132,8 @@ class Settings(BaseSettings): app_name: str = "OOP Portal Backend" app_version: str app_description: str = "Portal backend API for the Open Operator Platform" # Directory of the built GUI, served at /. Empty in dev, where Vite serves it. gui_dir: str = "" auth: AuthSettings = AuthSettings() oidc: OIDCSettings | None = None Loading src/portal_service/main.py +5 −0 Changes for src/portal_service/main.py: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ from portal_service.api.dependencies import ( get_oeg_access_token, ) from portal_service.api.errors import register_exception_handlers from portal_service.api.gui import GuiStaticFiles from portal_service.api.health import health_router from portal_service.api.middlewares import register_middlewares from portal_service.api.oeg.routes import oeg_router Loading Loading @@ -145,4 +146,8 @@ def create_app() -> FastAPI: app.dependency_overrides[get_oeg_access_token] = lambda: None logger.warning("security disabled: every request acts as the dev user") if settings.gui_dir: # Mounted last, so the routes above take precedence. app.mount("/", GuiStaticFiles(directory=settings.gui_dir, html=True), name="gui") return app Loading
.dockerignore 0 → 100644 +10 −0 Changes for .dockerignore: 10 added lines, 0 removed lines. Original line number Diff line number Diff line * !pyproject.toml !uv.lock !README.md !src/portal_service/ !portal-gui/ **/__pycache__ portal-gui/node_modules portal-gui/dist portal-gui/.env*.local
Dockerfile 0 → 100644 +25 −0 Changes for Dockerfile: 25 added lines, 0 removed lines. Original line number Diff line number Diff line FROM node:22-slim AS gui WORKDIR /gui COPY portal-gui/package.json portal-gui/package-lock.json ./ RUN npm ci --no-audit --no-fund COPY portal-gui/ ./ RUN npm run build FROM python:3.12-slim AS backend RUN pip install --no-cache-dir uv WORKDIR /app ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy COPY pyproject.toml uv.lock ./ RUN uv sync --locked --no-install-project COPY README.md ./ COPY src/ src/ RUN uv sync --locked --no-editable FROM python:3.12-slim WORKDIR /app COPY --from=backend /app/.venv .venv COPY --from=gui /gui/dist gui ENV PATH=/app/.venv/bin:$PATH GUI_DIR=/app/gui USER 65534 EXPOSE 8000 CMD ["uvicorn", "portal_service.main:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"]
src/portal_service/api/gui.py 0 → 100644 +20 −0 Changes for src/portal_service/api/gui.py: 20 added lines, 0 removed lines. Original line number Diff line number Diff line from starlette.exceptions import HTTPException from starlette.responses import Response from starlette.staticfiles import StaticFiles from starlette.types import Scope class GuiStaticFiles(StaticFiles): """The built GUI. Unknown paths get index.html, so client-side routes survive a reload.""" async def get_response(self, path: str, scope: Scope) -> Response: try: response = await super().get_response(path, scope) except HTTPException as e: if e.status_code != 404 or path.startswith("api/"): raise response = await super().get_response("index.html", scope) # Keeps signed-in pages out of the back-forward cache, as the Vite server does. if response.media_type == "text/html": response.headers["Cache-Control"] = "no-store" return response
src/portal_service/config.py +2 −0 Changes for src/portal_service/config.py: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -132,6 +132,8 @@ class Settings(BaseSettings): app_name: str = "OOP Portal Backend" app_version: str app_description: str = "Portal backend API for the Open Operator Platform" # Directory of the built GUI, served at /. Empty in dev, where Vite serves it. gui_dir: str = "" auth: AuthSettings = AuthSettings() oidc: OIDCSettings | None = None Loading
src/portal_service/main.py +5 −0 Changes for src/portal_service/main.py: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ from portal_service.api.dependencies import ( get_oeg_access_token, ) from portal_service.api.errors import register_exception_handlers from portal_service.api.gui import GuiStaticFiles from portal_service.api.health import health_router from portal_service.api.middlewares import register_middlewares from portal_service.api.oeg.routes import oeg_router Loading Loading @@ -145,4 +146,8 @@ def create_app() -> FastAPI: app.dependency_overrides[get_oeg_access_token] = lambda: None logger.warning("security disabled: every request acts as the dev user") if settings.gui_dir: # Mounted last, so the routes above take precedence. app.mount("/", GuiStaticFiles(directory=settings.gui_dir, html=True), name="gui") return app