From 30dc0b75dfca6b579cce864347617bae16e53af5 Mon Sep 17 00:00:00 2001 From: zvx-echo6 Date: Tue, 12 May 2026 12:36:59 -0600 Subject: [PATCH] fix(dashboard): SPA catch-all route for client-side routing - Add catch-all route that serves index.html for any non-API path - Mount /assets separately for static JS/CSS files - Enables browser refresh on React Router paths (/mesh, /config, etc.) - API routes (/api/*) continue to work normally Co-Authored-By: Claude Opus 4.5 --- meshai/dashboard/server.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/meshai/dashboard/server.py b/meshai/dashboard/server.py index 4217139..1302b61 100644 --- a/meshai/dashboard/server.py +++ b/meshai/dashboard/server.py @@ -7,8 +7,9 @@ from pathlib import Path from typing import TYPE_CHECKING import uvicorn -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from .ws import DashboardBroadcaster, router as ws_router @@ -61,10 +62,31 @@ def create_app() -> FastAPI: # WebSocket router (no prefix, path is /ws/live) app.include_router(ws_router) - # Static files - mount LAST so /api routes take priority + # Static files setup for SPA static_dir = Path(__file__).parent / "static" + index_html = static_dir / "index.html" + if static_dir.exists(): - app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static") + # Mount /assets for JS, CSS, images + assets_dir = static_dir / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets") + + # SPA catch-all: serve index.html for any non-API, non-static path + # This enables React Router client-side routing + @app.get("/{path:path}") + async def spa_catch_all(request: Request, path: str): + # Let static files be served directly if they exist + file_path = static_dir / path + if file_path.is_file(): + return FileResponse(file_path) + # Otherwise serve index.html for client-side routing + return FileResponse(index_html) + + # Explicit root route + @app.get("/") + async def root(): + return FileResponse(index_html) return app