/ src / web_frontend_server.py
web_frontend_server.py
 1  """
 2  Production frontend server.
 3  
 4  Serves the pre-built React bundle from /web_dist with SPA client-side routing.
 5  Used by the web container in production mode (AG3NTUM_MODE=prod).
 6  In development mode, the Vite dev server is used instead.
 7  
 8  Routing: if the request path matches a static file under the dist directory,
 9  serve it. Otherwise, serve index.html for React Router client-side routing.
10  
11  Set WEB_DIST_DIR env var to override the dist directory (used in tests).
12  """
13  import os
14  from pathlib import Path
15  
16  from starlette.applications import Starlette
17  from starlette.requests import Request
18  from starlette.responses import FileResponse, Response
19  from starlette.routing import Route
20  
21  
22  def _get_dist_dir() -> Path:
23      """Return the web distribution directory path."""
24      return Path(os.environ.get("WEB_DIST_DIR", "/web_dist"))
25  
26  
27  async def serve(request: Request) -> Response:
28      """Serve static files with SPA fallback to index.html."""
29      dist = _get_dist_dir()
30      path = request.path_params.get("path", "")
31  
32      if path:
33          file_path = (dist / path).resolve()
34          # Serve the file if it exists and is within the dist directory
35          if file_path.is_file() and str(file_path).startswith(str(dist.resolve())):
36              return FileResponse(str(file_path))
37  
38      # SPA fallback: serve index.html for all unmatched paths
39      index = dist / "index.html"
40      if not index.is_file():
41          return Response(
42              "Frontend not built. Run: ./run.sh build",
43              status_code=503,
44              media_type="text/plain",
45          )
46      return FileResponse(str(index), media_type="text/html")
47  
48  
49  app = Starlette(routes=[
50      Route("/", endpoint=serve),
51      Route("/{path:path}", endpoint=serve),
52  ])