feat(host-agent): serve built console via SPA-aware static mount
Add an optional single-process mode where the backend serves the built console bundle itself, so operators don't need a separate `npm run dev` for edge/dev setups. When RUNTIME_CONSOLE_STATIC_DIR points at the console dist directory, the app mounts a SpaStaticFiles handler at /ui/ (with 404 fallback to index.html for client-side routing) and redirects / to /ui/. The console build uses an empty VITE_API_BASE_URL for relative API paths (same-origin, no CORS), and Vite's base is set to /ui/ so assets resolve under the mount. /console/* JSON API is unchanged and is shared by both serve modes. api.ts now treats an explicitly-empty VITE_API_BASE_URL as "use relative paths" instead of falling back to the dev default, which previously forced absolute URLs even in same-origin builds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+40
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from api.console import create_console_router
|
||||
@@ -25,7 +27,27 @@ def create_app(
|
||||
) -> Any:
|
||||
from fastapi import BackgroundTasks, FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.types import Scope
|
||||
|
||||
class SpaStaticFiles(StaticFiles):
|
||||
"""``StaticFiles`` variant that falls back to ``index.html`` for SPA routes.
|
||||
|
||||
Mirrors ``apps/cloud-api/cloud_api/app.py``'s implementation: an unknown
|
||||
path like ``/ui/tasks/abc`` would otherwise 404 instead of letting the
|
||||
SPA's client-side router handle it.
|
||||
"""
|
||||
|
||||
async def get_response(self, path: str, scope: Scope) -> Any:
|
||||
try:
|
||||
return await super().get_response(path, scope)
|
||||
except StarletteHTTPException as exc:
|
||||
if exc.status_code == 404 and path != "index.html":
|
||||
return await super().get_response("index.html", scope)
|
||||
raise
|
||||
|
||||
device_manager = manager or DEFAULT_MANAGER
|
||||
store = metadata_store or TaskMetadataStore()
|
||||
@@ -127,6 +149,24 @@ def create_app(
|
||||
)
|
||||
)
|
||||
|
||||
console_static_dir = os.environ.get("RUNTIME_CONSOLE_STATIC_DIR")
|
||||
if console_static_dir:
|
||||
dist_dir = Path(console_static_dir)
|
||||
if not dist_dir.is_dir():
|
||||
raise ValueError(
|
||||
f"RUNTIME_CONSOLE_STATIC_DIR is not a directory: {dist_dir}"
|
||||
)
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def _redirect_to_console() -> RedirectResponse:
|
||||
return RedirectResponse(url="/ui/")
|
||||
|
||||
app.mount(
|
||||
"/ui",
|
||||
SpaStaticFiles(directory=str(dist_dir), html=True),
|
||||
name="console",
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user