update main to v0.10.0 #48
+8
-1
@@ -18,7 +18,7 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from backend.database import engine, Base, get_db
|
from backend.database import engine, Base, get_db
|
||||||
from backend.routers import roster, units, photos, roster_edit, roster_rename, dashboard, dashboard_tabs, activity, slmm, slm_ui, slm_dashboard, seismo_dashboard, projects, project_locations, scheduler, modem_dashboard
|
from backend.routers import roster, units, photos, roster_edit, roster_rename, dashboard, dashboard_tabs, activity, slmm, slm_ui, slm_dashboard, seismo_dashboard, sfm, projects, project_locations, scheduler, modem_dashboard
|
||||||
from backend.services.snapshot import emit_status_snapshot
|
from backend.services.snapshot import emit_status_snapshot
|
||||||
from backend.models import IgnoredUnit
|
from backend.models import IgnoredUnit
|
||||||
from backend.utils.timezone import get_user_timezone
|
from backend.utils.timezone import get_user_timezone
|
||||||
@@ -97,6 +97,7 @@ app.include_router(slmm.router)
|
|||||||
app.include_router(slm_ui.router)
|
app.include_router(slm_ui.router)
|
||||||
app.include_router(slm_dashboard.router)
|
app.include_router(slm_dashboard.router)
|
||||||
app.include_router(seismo_dashboard.router)
|
app.include_router(seismo_dashboard.router)
|
||||||
|
app.include_router(sfm.router)
|
||||||
app.include_router(modem_dashboard.router)
|
app.include_router(modem_dashboard.router)
|
||||||
|
|
||||||
from backend.routers import settings
|
from backend.routers import settings
|
||||||
@@ -233,6 +234,12 @@ async def seismographs_page(request: Request):
|
|||||||
return templates.TemplateResponse("seismographs.html", {"request": request})
|
return templates.TemplateResponse("seismographs.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sfm", response_class=HTMLResponse)
|
||||||
|
async def sfm_page(request: Request):
|
||||||
|
"""SFM live event data and device control dashboard"""
|
||||||
|
return templates.TemplateResponse("sfm.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/modems", response_class=HTMLResponse)
|
@app.get("/modems", response_class=HTMLResponse)
|
||||||
async def modems_page(request: Request):
|
async def modems_page(request: Request):
|
||||||
"""Field modems management dashboard"""
|
"""Field modems management dashboard"""
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""
|
||||||
|
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)}"
|
||||||
|
)
|
||||||
@@ -122,6 +122,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Seismographs
|
Seismographs
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/sfm" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/sfm' %}bg-gray-100 dark:bg-gray-700{% endif %}">
|
||||||
|
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||||
|
</svg>
|
||||||
|
SFM Live Data
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="/sound-level-meters" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/sound-level-meters' %}bg-gray-100 dark:bg-gray-700{% endif %}">
|
<a href="/sound-level-meters" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/sound-level-meters' %}bg-gray-100 dark:bg-gray-700{% endif %}">
|
||||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
+1117
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user