Migration cleanup: SLM dashboard restored, db migration
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
241
app/slm/dashboard.py
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user