|
24 | 24 | from pydantic import field_validator |
25 | 25 |
|
26 | 26 | from fastapi import FastAPI, File, Form, HTTPException, UploadFile, Header, Query, Depends, Request |
27 | | -from fastapi.responses import RedirectResponse |
| 27 | +from fastapi.responses import FileResponse, RedirectResponse |
28 | 28 | from fastapi.middleware.cors import CORSMiddleware |
29 | 29 | from fastapi.staticfiles import StaticFiles |
30 | 30 | from starlette.middleware.base import BaseHTTPMiddleware |
@@ -459,10 +459,13 @@ async def lifespan(_app: FastAPI): |
459 | 459 |
|
460 | 460 | # ===== Core Endpoints ===== |
461 | 461 |
|
462 | | - @app.get("/") |
463 | | - def root() -> RedirectResponse: |
464 | | - """Redirect to API docs when no frontend is built.""" |
465 | | - return RedirectResponse(url="/docs", status_code=302) |
| 462 | + _frontend_dist = Path(__file__).resolve().parents[3] / "frontend" / "dist" |
| 463 | + |
| 464 | + if not (_frontend_dist / "index.html").exists(): |
| 465 | + @app.get("/") |
| 466 | + def root() -> RedirectResponse: |
| 467 | + """Redirect to API docs when no frontend is built.""" |
| 468 | + return RedirectResponse(url="/docs", status_code=302) |
466 | 469 |
|
467 | 470 | @app.get("/api/health") |
468 | 471 | def health() -> dict[str, Any]: |
@@ -1198,11 +1201,20 @@ def list_pending_alerts( |
1198 | 1201 |
|
1199 | 1202 | # ===== Static Files ===== |
1200 | 1203 |
|
1201 | | - # Serve built frontend when dist exists (production mode) |
1202 | | - frontend_dir = Path(__file__).resolve().parents[3] / "frontend" |
1203 | | - dist_dir = frontend_dir / "dist" |
1204 | | - if dist_dir.exists(): |
1205 | | - app.mount("/", StaticFiles(directory=str(dist_dir), html=True), name="frontend") |
| 1204 | + # Serve built frontend when dist exists (production mode). |
| 1205 | + # Registered last so all API routes above take precedence. Unknown paths |
| 1206 | + # fall back to index.html so React Router deep links (/runs, /upload, ...) |
| 1207 | + # work on direct navigation and refresh. |
| 1208 | + dist_dir = _frontend_dist |
| 1209 | + if (dist_dir / "index.html").exists(): |
| 1210 | + app.mount("/assets", StaticFiles(directory=str(dist_dir / "assets")), name="assets") |
| 1211 | + |
| 1212 | + @app.get("/{full_path:path}", include_in_schema=False) |
| 1213 | + def frontend(full_path: str) -> FileResponse: |
| 1214 | + candidate = dist_dir / full_path |
| 1215 | + if full_path and ".." not in full_path and candidate.is_file(): |
| 1216 | + return FileResponse(candidate) |
| 1217 | + return FileResponse(dist_dir / "index.html") |
1206 | 1218 |
|
1207 | 1219 | return app |
1208 | 1220 |
|
|
0 commit comments