"""Hugging Face Gradio Space entrypoint for SignalWatch. HF free tier only supports the Gradio SDK. We merge our FastAPI backend (live simulator, anomaly scoring, alerts) and the built React dashboard into a single app, then let the platform's Gradio runtime serve it — no manual uvicorn, so there is no port conflict. """ import os import sys from pathlib import Path SPACE_ROOT = Path(__file__).parent BACKEND_DIR = SPACE_ROOT / "backend" STATIC_DIR = SPACE_ROOT / "static" sys.path.insert(0, str(BACKEND_DIR)) os.chdir(BACKEND_DIR) import gradio as gr # noqa: E402 from starlette.responses import FileResponse, JSONResponse # noqa: E402 from starlette.staticfiles import StaticFiles # noqa: E402 from starlette.exceptions import HTTPException as StarletteHTTPException # noqa: E402 from main import app # noqa: E402 FastAPI app with all /api routes + lifespan # ---- Serve the built React dashboard ------------------------------ app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="assets") @app.get("/", include_in_schema=False) async def index(): return FileResponse(STATIC_DIR / "index.html") @app.exception_handler(StarletteHTTPException) async def spa_fallback(request, exc): p = request.url.path if exc.status_code == 404 and not p.startswith("/api") and not p.startswith("/gradio"): if (STATIC_DIR / "index.html").exists(): return FileResponse(STATIC_DIR / "index.html") return JSONResponse({"detail": "Not Found"}, status_code=404) # ---- Gradio Blocks ------------------------------------------------- # Exists to satisfy the SDK; the real UI is the React SPA served at /. with gr.Blocks(title="SignalWatch") as demo: gr.Markdown("# 📡 SignalWatch") gr.HTML( '

' "Loading SignalWatch dashboard…

" ) # Merge: Gradio serves everything (its own /gradio + our / and /api). app = gr.mount_gradio_app(app, demo, path="/gradio")