Skip to content

Commit 1b3f7c0

Browse files
committed
fix(api): serve React frontend at root instead of redirecting to /docs
The unconditional '/' route redirected to Swagger before the static mount could serve index.html, so the deployed site showed API docs instead of the UI. Register the redirect only when no frontend build exists, and add an SPA fallback so deep links (/runs, /upload, ...) resolve to index.html on direct navigation.
1 parent eabb3eb commit 1b3f7c0

1 file changed

Lines changed: 22 additions & 10 deletions

File tree

src/climatevision/api/main.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from pydantic import field_validator
2525

2626
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
2828
from fastapi.middleware.cors import CORSMiddleware
2929
from fastapi.staticfiles import StaticFiles
3030
from starlette.middleware.base import BaseHTTPMiddleware
@@ -459,10 +459,13 @@ async def lifespan(_app: FastAPI):
459459

460460
# ===== Core Endpoints =====
461461

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)
466469

467470
@app.get("/api/health")
468471
def health() -> dict[str, Any]:
@@ -1198,11 +1201,20 @@ def list_pending_alerts(
11981201

11991202
# ===== Static Files =====
12001203

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")
12061218

12071219
return app
12081220

0 commit comments

Comments
 (0)