architecture: remove redundant SFM service and simplify deployment

This commit is contained in:
serversdwn
2026-01-09 20:58:16 +00:00
parent 94354da611
commit 7715123053
6 changed files with 207 additions and 149 deletions

View File

@@ -2,108 +2,145 @@
Dashboard API endpoints for SLM/NL43 devices.
This layer aggregates and transforms data from the device API for UI consumption.
"""
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List, Dict, Any
import logging
from app.slm.database import get_db
from app.slm.database import get_db as get_slm_db
from app.slm.models import NL43Config, NL43Status
from app.slm.services import NL43Client
# Import seismo database for roster data
from app.seismo.database import get_db as get_seismo_db
from app.seismo.models import RosterUnit
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/slm-dashboard", tags=["slm-dashboard"])
templates = Jinja2Templates(directory="app/ui/templates")
@router.get("/stats")
async def get_dashboard_stats(db: Session = Depends(get_db)):
"""Get aggregate statistics for the SLM dashboard."""
total_units = db.query(func.count(NL43Config.unit_id)).scalar() or 0
@router.get("/stats", response_class=HTMLResponse)
async def get_dashboard_stats(request: Request, db: Session = Depends(get_seismo_db)):
"""Get aggregate statistics for the SLM dashboard from roster (returns HTML)."""
# Query SLMs from the roster
slms = db.query(RosterUnit).filter_by(
device_type="sound_level_meter",
retired=False
).all()
# Count units with recent status updates (within last 5 minutes)
from datetime import datetime, timedelta
five_min_ago = datetime.utcnow() - timedelta(minutes=5)
online_units = db.query(func.count(NL43Status.unit_id)).filter(
NL43Status.last_seen >= five_min_ago
).scalar() or 0
total_units = len(slms)
deployed = sum(1 for s in slms if s.deployed)
benched = sum(1 for s in slms if not s.deployed)
# Count units currently measuring
measuring_units = db.query(func.count(NL43Status.unit_id)).filter(
NL43Status.measurement_state == "Measure"
).scalar() or 0
# For "active", count SLMs with recent check-ins (within last hour)
from datetime import datetime, timedelta, timezone
one_hour_ago = datetime.now(timezone.utc) - timedelta(hours=1)
active = sum(1 for s in slms if s.slm_last_check and s.slm_last_check >= one_hour_ago)
return {
"total_units": total_units,
"online_units": online_units,
"offline_units": total_units - online_units,
"measuring_units": measuring_units,
"idle_units": online_units - measuring_units
}
# Map to template variable names
# total_count, deployed_count, active_count, benched_count
return templates.TemplateResponse(
"partials/slm_stats.html",
{
"request": request,
"total_count": total_units,
"deployed_count": deployed,
"active_count": active,
"benched_count": benched
}
)
@router.get("/units")
async def get_units_list(db: Session = Depends(get_db)):
"""Get list of all NL43 units with their latest status."""
configs = db.query(NL43Config).all()
@router.get("/units", response_class=HTMLResponse)
async def get_units_list(request: Request, db: Session = Depends(get_seismo_db)):
"""Get list of all SLM units from roster (returns HTML)."""
# Query SLMs from the roster (not retired)
slms = db.query(RosterUnit).filter_by(
device_type="sound_level_meter",
retired=False
).order_by(RosterUnit.id).all()
units = []
for config in configs:
status = db.query(NL43Status).filter_by(unit_id=config.unit_id).first()
# Determine if unit is online (status updated within last 5 minutes)
from datetime import datetime, timedelta
is_online = False
if status and status.last_seen:
five_min_ago = datetime.utcnow() - timedelta(minutes=5)
is_online = status.last_seen >= five_min_ago
for slm in slms:
# Map to template field names
unit_data = {
"unit_id": config.unit_id,
"host": config.host,
"tcp_port": config.tcp_port,
"tcp_enabled": config.tcp_enabled,
"is_online": is_online,
"measurement_state": status.measurement_state if status else "unknown",
"last_seen": status.last_seen.isoformat() if status and status.last_seen else None,
"lp": status.lp if status else None,
"leq": status.leq if status else None,
"lmax": status.lmax if status else None,
"battery_level": status.battery_level if status else None,
"id": slm.id,
"slm_host": slm.slm_host,
"slm_tcp_port": slm.slm_tcp_port,
"slm_last_check": slm.slm_last_check,
"slm_model": slm.slm_model or "NL-43",
"address": slm.address,
"deployed_with_modem_id": slm.deployed_with_modem_id,
}
units.append(unit_data)
return {"units": units}
return templates.TemplateResponse(
"partials/slm_unit_list.html",
{
"request": request,
"units": units
}
)
@router.get("/live-view/{unit_id}")
async def get_live_view(unit_id: str, db: Session = Depends(get_db)):
"""Get live measurement data for a specific unit."""
status = db.query(NL43Status).filter_by(unit_id=unit_id).first()
if not status:
raise HTTPException(status_code=404, detail="Unit not found")
@router.get("/live-view/{unit_id}", response_class=HTMLResponse)
async def get_live_view(unit_id: str, request: Request, slm_db: Session = Depends(get_slm_db), roster_db: Session = Depends(get_seismo_db)):
"""Get live measurement data for a specific unit (returns HTML)."""
# Get unit from roster
unit = roster_db.query(RosterUnit).filter_by(
id=unit_id,
device_type="sound_level_meter"
).first()
return {
"unit_id": unit_id,
"last_seen": status.last_seen.isoformat() if status.last_seen else None,
"measurement_state": status.measurement_state,
"measurement_start_time": status.measurement_start_time.isoformat() if status.measurement_start_time else None,
"counter": status.counter,
"lp": status.lp,
"leq": status.leq,
"lmax": status.lmax,
"lmin": status.lmin,
"lpeak": status.lpeak,
"battery_level": status.battery_level,
"power_source": status.power_source,
"sd_remaining_mb": status.sd_remaining_mb,
"sd_free_ratio": status.sd_free_ratio,
}
if not unit:
return templates.TemplateResponse(
"partials/slm_live_view_error.html",
{
"request": request,
"error": f"Unit {unit_id} not found in roster"
}
)
# Get status from monitoring database (may not exist yet)
status = slm_db.query(NL43Status).filter_by(unit_id=unit_id).first()
# Get modem info if available
modem = None
modem_ip = None
if unit.deployed_with_modem_id:
modem = roster_db.query(RosterUnit).filter_by(
id=unit.deployed_with_modem_id,
device_type="modem"
).first()
if modem:
modem_ip = modem.ip_address
elif unit.slm_host:
modem_ip = unit.slm_host
# Determine if measuring
is_measuring = False
if status and status.measurement_state:
is_measuring = status.measurement_state.lower() == 'start'
return templates.TemplateResponse(
"partials/slm_live_view.html",
{
"request": request,
"unit": unit,
"modem": modem,
"modem_ip": modem_ip,
"current_status": status,
"is_measuring": is_measuring
}
)
@router.get("/config/{unit_id}")
async def get_unit_config(unit_id: str, db: Session = Depends(get_db)):
async def get_unit_config(unit_id: str, db: Session = Depends(get_slm_db)):
"""Get configuration for a specific unit."""
config = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not config:
@@ -122,7 +159,7 @@ async def get_unit_config(unit_id: str, db: Session = Depends(get_db)):
@router.post("/config/{unit_id}")
async def update_unit_config(unit_id: str, config_data: Dict[str, Any], db: Session = Depends(get_db)):
async def update_unit_config(unit_id: str, config_data: Dict[str, Any], db: Session = Depends(get_slm_db)):
"""Update configuration for a specific unit."""
config = db.query(NL43Config).filter_by(unit_id=unit_id).first()
@@ -154,7 +191,7 @@ async def update_unit_config(unit_id: str, config_data: Dict[str, Any], db: Sess
@router.post("/control/{unit_id}/{action}")
async def control_unit(unit_id: str, action: str, db: Session = Depends(get_db)):
async def control_unit(unit_id: str, action: str, db: Session = Depends(get_slm_db)):
"""Send control command to a unit (start, stop, pause, resume, etc.)."""
config = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not config:
@@ -201,7 +238,7 @@ async def control_unit(unit_id: str, action: str, db: Session = Depends(get_db))
@router.get("/test-modem/{unit_id}")
async def test_modem(unit_id: str, db: Session = Depends(get_db)):
async def test_modem(unit_id: str, db: Session = Depends(get_slm_db)):
"""Test connectivity to a unit's modem/device."""
config = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not config: