""" SFM (Seismograph Field Module) Proxy Router Proxies requests from terra-view to the standalone SFM backend service. SFM runs on port 8200 and handles MiniMate Plus seismograph communication and event database queries. SFM endpoints are at root level (e.g. /db/units, /device/info) — no /api/ prefix. """ from fastapi import APIRouter, HTTPException, Request, Response import httpx import logging import os logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/sfm", tags=["sfm"]) # SFM backend URL - configurable via environment variable SFM_BASE_URL = os.getenv("SFM_BASE_URL", "http://localhost:8200") @router.get("/health") async def check_sfm_health(): """ Check if the SFM backend service is reachable and healthy. """ try: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get(f"{SFM_BASE_URL}/health") if response.status_code == 200: data = response.json() return { "status": "ok", "sfm_status": "connected", "sfm_url": SFM_BASE_URL, "sfm_response": data } else: return { "status": "degraded", "sfm_status": "error", "sfm_url": SFM_BASE_URL, "detail": f"SFM returned status {response.status_code}" } except httpx.ConnectError: return { "status": "error", "sfm_status": "unreachable", "sfm_url": SFM_BASE_URL, "detail": "Cannot connect to SFM backend. Is it running?" } except Exception as e: return { "status": "error", "sfm_status": "error", "sfm_url": SFM_BASE_URL, "detail": str(e) } # HTTP catch-all — proxies everything else to SFM backend @router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) async def proxy_to_sfm(path: str, request: Request): """ Proxy all requests to the SFM backend service. SFM endpoints have no /api/ prefix — target URL is {SFM_BASE_URL}/{path}. Timeout is 60s to allow for live device round-trips (event downloads can take 30-45s for a full event list). """ # Build target URL — SFM endpoints live at root, not /api/ target_url = f"{SFM_BASE_URL}/{path}" # Forward query params query_params = dict(request.query_params) # Read body for mutation requests body = None if request.method in ["POST", "PUT", "PATCH"]: try: body = await request.body() except Exception as e: logger.error(f"Failed to read request body: {e}") body = None # Strip hop-by-hop headers headers = dict(request.headers) headers_to_exclude = ["host", "content-length", "transfer-encoding", "connection"] proxy_headers = {k: v for k, v in headers.items() if k.lower() not in headers_to_exclude} logger.info(f"Proxying {request.method} {path} → SFM: {target_url}") try: async with httpx.AsyncClient(timeout=60.0) as client: response = await client.request( method=request.method, url=target_url, params=query_params, headers=proxy_headers, content=body ) return Response( content=response.content, status_code=response.status_code, headers=dict(response.headers), media_type=response.headers.get("content-type") ) except httpx.ConnectError: logger.error(f"Failed to connect to SFM backend at {SFM_BASE_URL}") raise HTTPException( status_code=503, detail=f"SFM backend service unavailable. Is SFM running on {SFM_BASE_URL}?" ) except httpx.TimeoutException: logger.error(f"Timeout connecting to SFM backend at {SFM_BASE_URL}") raise HTTPException( status_code=504, detail="SFM backend timeout" ) except Exception as e: logger.error(f"Error proxying to SFM: {e}") raise HTTPException( status_code=500, detail=f"Failed to proxy request to SFM: {str(e)}" )