devrajsinh2012 commited on
Commit
537f246
Β·
1 Parent(s): 68fa974

feat: serve full-stack app from single HF Space container

Browse files

- Dockerfile: add Node 20 build stage to compile React frontend,
copy dist/ into /app/static/ in final Python image
- main.py: mount /assets static dir + root & SPA catch-all routes
that serve index.html so React Router works end-to-end
- useWebSocket.ts: auto-derive WS URL from window.location (wss when
served over HTTPS, e.g. HF Space) β€” no VITE_WS_URL needed
- requirements.txt: add aiofiles (required by StaticFiles middleware)

Dockerfile CHANGED
@@ -1,11 +1,23 @@
1
  # ─────────────────────────────────────────────────────────────────────────────
2
- # SanketSetu Backend β€” Dockerfile
3
  # Build context: repo root (SanketSetu/)
4
  #
5
- # docker build -t sanketsetu-backend .
6
- # docker run -p 8000:8000 sanketsetu-backend
7
  # ─────────────────────────────────────────────────────────────────────────────
8
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  FROM python:3.12-slim AS base
10
 
11
  # System libraries needed by OpenCV headless + Pillow
@@ -21,6 +33,9 @@ RUN pip install --no-cache-dir -r requirements.txt
21
  # ── Application source ────────────────────────────────────────────────────────
22
  COPY backend/app/ ./app/
23
 
 
 
 
24
  # ── Model artefacts ───────────────────────────────────────────────────────────
25
  # Copied to /models so the container is fully self-contained.
26
  # Override at runtime with -e WEIGHTS_DIR=/mnt/models + bind-mount if preferred.
 
1
  # ─────────────────────────────────────────────────────────────────────────────
2
+ # SanketSetu β€” Dockerfile (full-stack: React frontend + FastAPI backend)
3
  # Build context: repo root (SanketSetu/)
4
  #
5
+ # docker build -t sanketsetu .
6
+ # docker run -p 7860:7860 sanketsetu
7
  # ─────────────────────────────────────────────────────────────────────────────
8
 
9
+ # ── Stage 1: Build React frontend ────────────────────────────────────────────
10
+ FROM node:20-slim AS frontend-builder
11
+
12
+ WORKDIR /frontend
13
+ COPY frontend/package.json frontend/package-lock.json* ./
14
+ RUN npm ci --prefer-offline
15
+
16
+ COPY frontend/ ./
17
+ # No VITE_WS_URL β€” the hook derives it from window.location at runtime
18
+ RUN npm run build
19
+
20
+ # ── Stage 2: Python backend ───────────────────────────────────────────────────
21
  FROM python:3.12-slim AS base
22
 
23
  # System libraries needed by OpenCV headless + Pillow
 
33
  # ── Application source ────────────────────────────────────────────────────────
34
  COPY backend/app/ ./app/
35
 
36
+ # ── Frontend static files (built in Stage 1) ─────────────────────────────────
37
+ COPY --from=frontend-builder /frontend/dist ./static/
38
+
39
  # ── Model artefacts ───────────────────────────────────────────────────────────
40
  # Copied to /models so the container is fully self-contained.
41
  # Override at runtime with -e WEIGHTS_DIR=/mnt/models + bind-mount if preferred.
backend/app/main.py CHANGED
@@ -31,7 +31,8 @@ except ImportError:
31
  import numpy as np
32
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request
33
  from fastapi.middleware.cors import CORSMiddleware
34
- from fastapi.responses import JSONResponse
 
35
 
36
  from app import config
37
  from app.models.loader import load_models, get_model_store
@@ -92,6 +93,11 @@ app.add_middleware(
92
  allow_headers=["*"],
93
  )
94
 
 
 
 
 
 
95
 
96
  # ---------------------------------------------------------------------------
97
  # Global exception handler
@@ -155,6 +161,18 @@ def _available_pipelines() -> list[str]:
155
  # REST endpoints
156
  # ---------------------------------------------------------------------------
157
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  @app.get("/health", response_model=HealthResponse)
159
  async def health():
160
  try:
@@ -278,3 +296,16 @@ async def ws_image(ws: WebSocket):
278
 
279
  except WebSocketDisconnect:
280
  logger.info("Image client disconnected: %s", session_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  import numpy as np
32
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request
33
  from fastapi.middleware.cors import CORSMiddleware
34
+ from fastapi.responses import JSONResponse, FileResponse
35
+ from fastapi.staticfiles import StaticFiles
36
 
37
  from app import config
38
  from app.models.loader import load_models, get_model_store
 
93
  allow_headers=["*"],
94
  )
95
 
96
+ # ── Serve React frontend static files (if built into /app/static) ────────────
97
+ _STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
98
+ if _STATIC_DIR.is_dir():
99
+ app.mount("/assets", StaticFiles(directory=str(_STATIC_DIR / "assets")), name="assets")
100
+
101
 
102
  # ---------------------------------------------------------------------------
103
  # Global exception handler
 
161
  # REST endpoints
162
  # ---------------------------------------------------------------------------
163
 
164
+ @app.get("/", include_in_schema=False)
165
+ @app.get("/index.html", include_in_schema=False)
166
+ async def serve_frontend():
167
+ """Serve the React SPA index for the root and any unknown path."""
168
+ index = _STATIC_DIR / "index.html"
169
+ if index.is_file():
170
+ return FileResponse(str(index), media_type="text/html")
171
+ # Fallback: redirect to API docs if frontend not bundled
172
+ from fastapi.responses import RedirectResponse
173
+ return RedirectResponse(url="/docs")
174
+
175
+
176
  @app.get("/health", response_model=HealthResponse)
177
  async def health():
178
  try:
 
296
 
297
  except WebSocketDisconnect:
298
  logger.info("Image client disconnected: %s", session_id)
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # SPA catch-all β€” must be LAST so it doesn't shadow API routes
303
+ # ---------------------------------------------------------------------------
304
+ @app.get("/{full_path:path}", include_in_schema=False)
305
+ async def serve_spa(full_path: str):
306
+ """Return index.html for any unknown path so React Router handles routing."""
307
+ index = _STATIC_DIR / "index.html"
308
+ if index.is_file():
309
+ return FileResponse(str(index), media_type="text/html")
310
+ from fastapi.responses import RedirectResponse
311
+ return RedirectResponse(url="/docs")
backend/requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  fastapi>=0.115.0
2
  uvicorn[standard]>=0.30.0
3
  websockets>=12.0
 
4
  xgboost>=2.0.0
5
  lightgbm>=4.3.0
6
  scikit-learn>=1.4.0
 
1
  fastapi>=0.115.0
2
  uvicorn[standard]>=0.30.0
3
  websockets>=12.0
4
+ aiofiles>=23.0.0
5
  xgboost>=2.0.0
6
  lightgbm>=4.3.0
7
  scikit-learn>=1.4.0
frontend/src/hooks/useWebSocket.ts CHANGED
@@ -1,7 +1,14 @@
1
  import { useEffect, useRef, useState, useCallback } from 'react';
2
  import type { PredictionResponse } from '../types';
3
 
4
- const WS_URL = import.meta.env.VITE_WS_URL ?? 'ws://localhost:8080';
 
 
 
 
 
 
 
5
  const RECONNECT_BASE_MS = 1000;
6
  const MAX_RECONNECT_MS = 30_000;
7
  const MAX_SEND_RATE = 15; // frames/sec β€” normal
 
1
  import { useEffect, useRef, useState, useCallback } from 'react';
2
  import type { PredictionResponse } from '../types';
3
 
4
+ // Derive WebSocket base URL from the current page origin so the hook works
5
+ // on any deployment (HF Space, Vercel + backend, localhost) without extra config.
6
+ function _defaultWsUrl(): string {
7
+ if (import.meta.env.VITE_WS_URL) return import.meta.env.VITE_WS_URL as string;
8
+ const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
9
+ return `${proto}://${window.location.host}`;
10
+ }
11
+ const WS_URL = _defaultWsUrl();
12
  const RECONNECT_BASE_MS = 1000;
13
  const MAX_RECONNECT_MS = 30_000;
14
  const MAX_SEND_RATE = 15; // frames/sec β€” normal