""" SLM Dashboard Router Provides API endpoints for the Sound Level Meters dashboard page. """ from fastapi import APIRouter, Request, Depends, Query from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session from sqlalchemy import func from datetime import datetime, timedelta import httpx import logging from backend.database import get_db from backend.models import RosterUnit logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/slm-dashboard", tags=["slm-dashboard"]) templates = Jinja2Templates(directory="templates") @router.get("/stats", response_class=HTMLResponse) async def get_slm_stats(request: Request, db: Session = Depends(get_db)): """ Get summary statistics for SLM dashboard. Returns HTML partial with stat cards. """ # Query all SLMs all_slms = db.query(RosterUnit).filter_by(device_type="sound_level_meter").all() # Count deployed vs benched deployed_count = sum(1 for slm in all_slms if slm.deployed and not slm.retired) benched_count = sum(1 for slm in all_slms if not slm.deployed and not slm.retired) retired_count = sum(1 for slm in all_slms if slm.retired) # Count recently active (checked in last hour) one_hour_ago = datetime.utcnow() - timedelta(hours=1) active_count = sum(1 for slm in all_slms if slm.slm_last_check and slm.slm_last_check > one_hour_ago) return templates.TemplateResponse("partials/slm_stats.html", { "request": request, "total_count": len(all_slms), "deployed_count": deployed_count, "benched_count": benched_count, "active_count": active_count, "retired_count": retired_count }) @router.get("/units", response_class=HTMLResponse) async def get_slm_units( request: Request, db: Session = Depends(get_db), search: str = Query(None) ): """ Get list of SLM units for the sidebar. Returns HTML partial with unit cards. """ query = db.query(RosterUnit).filter_by(device_type="sound_level_meter") # Filter by search term if provided if search: search_term = f"%{search}%" query = query.filter( (RosterUnit.id.like(search_term)) | (RosterUnit.slm_model.like(search_term)) | (RosterUnit.address.like(search_term)) ) # Only show deployed units by default units = query.filter_by(deployed=True, retired=False).order_by(RosterUnit.id).all() return templates.TemplateResponse("partials/slm_unit_list.html", { "request": request, "units": units }) @router.get("/live-view/{unit_id}", response_class=HTMLResponse) async def get_live_view(request: Request, unit_id: str, db: Session = Depends(get_db)): """ Get live view panel for a specific SLM unit. Returns HTML partial with live metrics and chart. """ # Get unit from database unit = db.query(RosterUnit).filter_by(id=unit_id, device_type="sound_level_meter").first() if not unit: return templates.TemplateResponse("partials/slm_live_view_error.html", { "request": request, "error": f"Unit {unit_id} not found" }) # Get modem information if assigned modem = None modem_ip = None if unit.deployed_with_modem_id: modem = db.query(RosterUnit).filter_by(id=unit.deployed_with_modem_id, device_type="modem").first() if modem: modem_ip = modem.ip_address else: logger.warning(f"SLM {unit_id} is assigned to modem {unit.deployed_with_modem_id} but modem not found") # Fallback to direct slm_host if no modem assigned (backward compatibility) if not modem_ip and unit.slm_host: modem_ip = unit.slm_host logger.info(f"Using legacy slm_host for {unit_id}: {modem_ip}") # Try to get current status from SLMM current_status = None measurement_state = None is_measuring = False try: async with httpx.AsyncClient(timeout=5.0) as client: # Get measurement state state_response = await client.get( f"http://localhost:8100/api/nl43/{unit_id}/measurement-state" ) if state_response.status_code == 200: state_data = state_response.json() measurement_state = state_data.get("measurement_state", "Unknown") is_measuring = state_data.get("is_measuring", False) # Get live status status_response = await client.get( f"http://localhost:8100/api/nl43/{unit_id}/live" ) if status_response.status_code == 200: status_data = status_response.json() current_status = status_data.get("data", {}) except Exception as e: logger.error(f"Failed to get status for {unit_id}: {e}") return templates.TemplateResponse("partials/slm_live_view.html", { "request": request, "unit": unit, "modem": modem, "modem_ip": modem_ip, "current_status": current_status, "measurement_state": measurement_state, "is_measuring": is_measuring }) @router.post("/control/{unit_id}/{action}") async def control_slm(unit_id: str, action: str): """ Send control commands to SLM (start, stop, pause, resume, reset). Proxies to SLMM backend. """ valid_actions = ["start", "stop", "pause", "resume", "reset"] if action not in valid_actions: return {"status": "error", "detail": f"Invalid action. Must be one of: {valid_actions}"} try: async with httpx.AsyncClient(timeout=10.0) as client: response = await client.post( f"http://localhost:8100/api/nl43/{unit_id}/{action}" ) if response.status_code == 200: return response.json() else: return { "status": "error", "detail": f"SLMM returned status {response.status_code}" } except Exception as e: logger.error(f"Failed to control {unit_id}: {e}") return { "status": "error", "detail": str(e) } @router.get("/config/{unit_id}", response_class=HTMLResponse) async def get_slm_config(request: Request, unit_id: str, db: Session = Depends(get_db)): """ Get configuration form for a specific SLM unit. Returns HTML partial with configuration form. """ unit = db.query(RosterUnit).filter_by(id=unit_id, device_type="sound_level_meter").first() if not unit: return HTMLResponse( content=f'