architecture: remove redundant SFM service and simplify deployment
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user