rajvivan commited on
Commit
ae84ddd
Β·
verified Β·
1 Parent(s): 98a5019

fix: unconditional SPA catch-all route + diagnostic fallback for missing frontend build

Browse files
Files changed (1) hide show
  1. backend/main.py +32 -26
backend/main.py CHANGED
@@ -5,7 +5,7 @@ from pathlib import Path
5
  import pandas as pd
6
  from fastapi import FastAPI, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
- from fastapi.responses import FileResponse
9
  from fastapi.staticfiles import StaticFiles
10
 
11
  from src.app.schemas import (
@@ -29,10 +29,11 @@ def _safe_int(val) -> int | None:
29
  except Exception:
30
  return None
31
 
 
32
  app = FastAPI(title="Traffic Incident Summarization API", version="0.4.0")
33
  app.add_middleware(
34
  CORSMiddleware,
35
- allow_origins=["*"],
36
  allow_credentials=True,
37
  allow_methods=["*"],
38
  allow_headers=["*"],
@@ -67,11 +68,9 @@ def get_samples(track: str = "gcc"):
67
  df = df[df["dataset_track"].fillna("us") == track]
68
  if df.empty:
69
  raise HTTPException(status_code=404, detail=f"No samples found for dataset track: {track}")
70
-
71
  if "Start_Time" in df.columns:
72
  df["Start_Time"] = pd.to_datetime(df["Start_Time"], errors="coerce")
73
  df = df.sort_values(by="Start_Time", ascending=False)
74
-
75
  # Pick one representative sample per severity level for a compact, diverse preview
76
  sev_map_int = {1: "Low", 2: "Medium", 3: "High", 4: "Critical"}
77
  severity_order = [3, 2, 4, 1] # High, Medium, Critical, Low β€” most interesting first
@@ -94,19 +93,15 @@ def get_samples(track: str = "gcc"):
94
  selected_rows.append(row)
95
  used_indices.add(row.name)
96
  sample_df = pd.DataFrame(selected_rows)
97
-
98
  items = []
99
  for idx, row in sample_df.iterrows():
100
  def clean(val):
101
  s = str(val).strip() if pd.notna(val) else ""
102
  return "" if s.lower() in ("nan", "none", "") else s
103
-
104
  loc_cols = ["road_name", "Street", "district", "City", "State", "emirate"]
105
  location_parts = [clean(row.get(col)) for col in loc_cols]
106
  title = " Β· ".join([p for p in location_parts if p][:3]) or f"Sample incident {idx + 1}"
107
-
108
  desc = clean(row.get("Description", "")) or "No description available."
109
-
110
  sev = row.get("Severity", "")
111
  if clean(str(sev)):
112
  if "severity" not in desc.lower():
@@ -116,11 +111,9 @@ def get_samples(track: str = "gcc"):
116
  desc = f"{desc} Classified as {sev_str} severity."
117
  except Exception:
118
  pass
119
-
120
  src_lbl = clean(row.get("source_label", ""))
121
  if not src_lbl:
122
  src_lbl = "US Accidents" if track == "us" else "GCC sample"
123
-
124
  items.append(
125
  SampleItem(
126
  id=str(idx + 1),
@@ -133,7 +126,6 @@ def get_samples(track: str = "gcc"):
133
  return SamplesResponse(items=items)
134
 
135
 
136
-
137
  @app.post("/summarize", response_model=SummarizeResponse)
138
  def summarize(request: SummarizeRequest):
139
  try:
@@ -163,20 +155,34 @@ def compare(request: CompareRequest):
163
  except Exception as exc:
164
  raise HTTPException(status_code=500, detail=str(exc)) from exc
165
 
 
166
  # ── Serve React Frontend (Single-Container Deployment e.g., Hugging Face) ──
167
  _DIST_PATH = Path(__file__).parent.parent / "frontend" / "dist"
168
-
169
- if _DIST_PATH.exists() and _DIST_PATH.is_dir():
170
- # Mount static assets first (CSS/JS files inside /assets)
171
- app.mount("/assets", StaticFiles(directory=str(_DIST_PATH / "assets")), name="assets")
172
-
173
- # Catch-all route for Single Page Application (React Router)
174
- @app.get("/{full_path:path}", response_class=FileResponse)
175
- async def serve_frontend(full_path: str):
176
- # Prevent accessing files outside dist/
177
- req_path = _DIST_PATH / full_path
178
- if req_path.exists() and req_path.is_file():
179
- return FileResponse(req_path)
180
- # Otherwise, fall back to React's index.html
181
- return FileResponse(_DIST_PATH / "index.html")
182
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import pandas as pd
6
  from fastapi import FastAPI, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import FileResponse, JSONResponse
9
  from fastapi.staticfiles import StaticFiles
10
 
11
  from src.app.schemas import (
 
29
  except Exception:
30
  return None
31
 
32
+
33
  app = FastAPI(title="Traffic Incident Summarization API", version="0.4.0")
34
  app.add_middleware(
35
  CORSMiddleware,
36
+ allow_origins=["*"],
37
  allow_credentials=True,
38
  allow_methods=["*"],
39
  allow_headers=["*"],
 
68
  df = df[df["dataset_track"].fillna("us") == track]
69
  if df.empty:
70
  raise HTTPException(status_code=404, detail=f"No samples found for dataset track: {track}")
 
71
  if "Start_Time" in df.columns:
72
  df["Start_Time"] = pd.to_datetime(df["Start_Time"], errors="coerce")
73
  df = df.sort_values(by="Start_Time", ascending=False)
 
74
  # Pick one representative sample per severity level for a compact, diverse preview
75
  sev_map_int = {1: "Low", 2: "Medium", 3: "High", 4: "Critical"}
76
  severity_order = [3, 2, 4, 1] # High, Medium, Critical, Low β€” most interesting first
 
93
  selected_rows.append(row)
94
  used_indices.add(row.name)
95
  sample_df = pd.DataFrame(selected_rows)
 
96
  items = []
97
  for idx, row in sample_df.iterrows():
98
  def clean(val):
99
  s = str(val).strip() if pd.notna(val) else ""
100
  return "" if s.lower() in ("nan", "none", "") else s
 
101
  loc_cols = ["road_name", "Street", "district", "City", "State", "emirate"]
102
  location_parts = [clean(row.get(col)) for col in loc_cols]
103
  title = " Β· ".join([p for p in location_parts if p][:3]) or f"Sample incident {idx + 1}"
 
104
  desc = clean(row.get("Description", "")) or "No description available."
 
105
  sev = row.get("Severity", "")
106
  if clean(str(sev)):
107
  if "severity" not in desc.lower():
 
111
  desc = f"{desc} Classified as {sev_str} severity."
112
  except Exception:
113
  pass
 
114
  src_lbl = clean(row.get("source_label", ""))
115
  if not src_lbl:
116
  src_lbl = "US Accidents" if track == "us" else "GCC sample"
 
117
  items.append(
118
  SampleItem(
119
  id=str(idx + 1),
 
126
  return SamplesResponse(items=items)
127
 
128
 
 
129
  @app.post("/summarize", response_model=SummarizeResponse)
130
  def summarize(request: SummarizeRequest):
131
  try:
 
155
  except Exception as exc:
156
  raise HTTPException(status_code=500, detail=str(exc)) from exc
157
 
158
+
159
  # ── Serve React Frontend (Single-Container Deployment e.g., Hugging Face) ──
160
  _DIST_PATH = Path(__file__).parent.parent / "frontend" / "dist"
161
+ _INDEX_HTML = _DIST_PATH / "index.html"
162
+ _ASSETS_PATH = _DIST_PATH / "assets"
163
+
164
+ # Mount static assets if the build exists
165
+ if _ASSETS_PATH.exists() and _ASSETS_PATH.is_dir():
166
+ app.mount("/assets", StaticFiles(directory=str(_ASSETS_PATH)), name="assets")
167
+
168
+
169
+ # Catch-all SPA route β€” always registered so HF Spaces never gets a 404 at root
170
+ @app.get("/{full_path:path}")
171
+ async def serve_frontend(full_path: str):
172
+ # Try to serve an exact file from dist (e.g. favicon.ico, robots.txt)
173
+ req_path = _DIST_PATH / full_path
174
+ if req_path.exists() and req_path.is_file():
175
+ return FileResponse(req_path)
176
+ # SPA fallback: always return index.html so React Router handles the path
177
+ if _INDEX_HTML.exists():
178
+ return FileResponse(_INDEX_HTML)
179
+ # Frontend build not found β€” return diagnostic JSON instead of 404
180
+ return JSONResponse(
181
+ status_code=200,
182
+ content={
183
+ "status": "api_only",
184
+ "message": "Frontend build not found. API is running.",
185
+ "api_docs": "/docs",
186
+ "health": "/health",
187
+ },
188
+ )