Migration cleanup: SLM dashboard restored, db migration

This commit is contained in:
serversdwn
2026-01-09 19:14:09 +00:00
parent ff438c1197
commit 5b907c0cd7
5 changed files with 365 additions and 4 deletions

View File

@@ -39,6 +39,7 @@ from app.seismo import routes as seismo_legacy_routes
# Import feature module routers (SLM)
from app.slm.routers import router as slm_router
from app.slm.dashboard import router as slm_dashboard_router
# Import API aggregation layer (placeholder for now)
from app.api import dashboard as api_dashboard
@@ -48,6 +49,9 @@ from app.api import roster as api_roster
from app.seismo.database import engine as seismo_engine, Base as SeismoBase
SeismoBase.metadata.create_all(bind=seismo_engine)
from app.slm.database import engine as slm_engine, Base as SlmBase
SlmBase.metadata.create_all(bind=slm_engine)
# Initialize FastAPI app
app = FastAPI(
title=APP_NAME,
@@ -104,6 +108,7 @@ app.include_router(seismo_legacy_routes.router)
# SLM Feature Module APIs
app.include_router(slm_router)
app.include_router(slm_dashboard_router)
# API Aggregation Layer (future cross-feature endpoints)
# app.include_router(api_dashboard.router) # TODO: Implement aggregation

View File

@@ -4,7 +4,7 @@ from fastapi.templating import Jinja2Templates
from app.seismo.services.snapshot import emit_status_snapshot
router = APIRouter()
templates = Jinja2Templates(directory="templates")
templates = Jinja2Templates(directory="app/ui/templates")
@router.get("/dashboard/active")

241
app/slm/dashboard.py Normal file
View File

@@ -0,0 +1,241 @@
"""
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 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.models import NL43Config, NL43Status
from app.slm.services import NL43Client
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/slm-dashboard", tags=["slm-dashboard"])
@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
# 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
# Count units currently measuring
measuring_units = db.query(func.count(NL43Status.unit_id)).filter(
NL43Status.measurement_state == "Measure"
).scalar() or 0
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
}
@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()
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
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,
}
units.append(unit_data)
return {"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")
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,
}
@router.get("/config/{unit_id}")
async def get_unit_config(unit_id: str, db: Session = Depends(get_db)):
"""Get configuration for a specific unit."""
config = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not config:
raise HTTPException(status_code=404, detail="Unit configuration not found")
return {
"unit_id": config.unit_id,
"host": config.host,
"tcp_port": config.tcp_port,
"tcp_enabled": config.tcp_enabled,
"ftp_enabled": config.ftp_enabled,
"ftp_username": config.ftp_username,
"ftp_password": config.ftp_password,
"web_enabled": config.web_enabled,
}
@router.post("/config/{unit_id}")
async def update_unit_config(unit_id: str, config_data: Dict[str, Any], db: Session = Depends(get_db)):
"""Update configuration for a specific unit."""
config = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not config:
# Create new config
config = NL43Config(unit_id=unit_id)
db.add(config)
# Update fields
if "host" in config_data:
config.host = config_data["host"]
if "tcp_port" in config_data:
config.tcp_port = config_data["tcp_port"]
if "tcp_enabled" in config_data:
config.tcp_enabled = config_data["tcp_enabled"]
if "ftp_enabled" in config_data:
config.ftp_enabled = config_data["ftp_enabled"]
if "ftp_username" in config_data:
config.ftp_username = config_data["ftp_username"]
if "ftp_password" in config_data:
config.ftp_password = config_data["ftp_password"]
if "web_enabled" in config_data:
config.web_enabled = config_data["web_enabled"]
db.commit()
db.refresh(config)
return {"success": True, "unit_id": unit_id}
@router.post("/control/{unit_id}/{action}")
async def control_unit(unit_id: str, action: str, db: Session = Depends(get_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:
raise HTTPException(status_code=404, detail="Unit configuration not found")
if not config.tcp_enabled:
raise HTTPException(status_code=400, detail="TCP control not enabled for this unit")
# Create NL43Client
client = NL43Client(
host=config.host,
port=config.tcp_port,
timeout=5.0,
ftp_username=config.ftp_username,
ftp_password=config.ftp_password
)
# Map action to command
action_map = {
"start": "start_measurement",
"stop": "stop_measurement",
"pause": "pause_measurement",
"resume": "resume_measurement",
"reset": "reset_measurement",
"sleep": "sleep_mode",
"wake": "wake_from_sleep",
}
if action not in action_map:
raise HTTPException(status_code=400, detail=f"Unknown action: {action}")
method_name = action_map[action]
method = getattr(client, method_name, None)
if not method:
raise HTTPException(status_code=500, detail=f"Method {method_name} not implemented")
try:
result = await method()
return {"success": True, "action": action, "result": result}
except Exception as e:
logger.error(f"Error executing {action} on {unit_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/test-modem/{unit_id}")
async def test_modem(unit_id: str, db: Session = Depends(get_db)):
"""Test connectivity to a unit's modem/device."""
config = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not config:
raise HTTPException(status_code=404, detail="Unit configuration not found")
if not config.tcp_enabled:
raise HTTPException(status_code=400, detail="TCP control not enabled for this unit")
client = NL43Client(
host=config.host,
port=config.tcp_port,
timeout=5.0,
ftp_username=config.ftp_username,
ftp_password=config.ftp_password
)
try:
# Try to get measurement state as a connectivity test
state = await client.get_measurement_state()
return {
"success": True,
"unit_id": unit_id,
"host": config.host,
"port": config.tcp_port,
"reachable": True,
"measurement_state": state
}
except Exception as e:
logger.warning(f"Modem test failed for {unit_id}: {e}")
return {
"success": False,
"unit_id": unit_id,
"host": config.host,
"port": config.tcp_port,
"reachable": False,
"error": str(e)
}