feat: Add Rename Unit functionality and improve navigation in SLM dashboard

- Implemented a modal for renaming units with validation and confirmation prompts.
- Added JavaScript functions to handle opening, closing, and submitting the rename unit form.
- Enhanced the back navigation in the SLM detail page to check referrer history.
- Updated breadcrumb navigation in the legacy dashboard to accommodate NRL locations.
- Improved the sound level meters page with a more informative header and device list.
- Introduced a live measurement chart with WebSocket support for real-time data streaming.
- Added functionality to manage active devices and projects with auto-refresh capabilities.
This commit is contained in:
serversdwn
2026-01-14 01:44:30 +00:00
parent e9216b9abc
commit be83cb3fe7
12 changed files with 1807 additions and 249 deletions

View File

@@ -0,0 +1,139 @@
"""
Roster Unit Rename Router
Provides endpoint for safely renaming unit IDs across all database tables.
"""
from fastapi import APIRouter, Depends, HTTPException, Form
from sqlalchemy.orm import Session
from datetime import datetime
import logging
from backend.database import get_db
from backend.models import RosterUnit, Emitter, UnitHistory
from backend.routers.roster_edit import record_history, sync_slm_to_slmm_cache
router = APIRouter(prefix="/api/roster", tags=["roster-rename"])
logger = logging.getLogger(__name__)
@router.post("/rename")
async def rename_unit(
old_id: str = Form(...),
new_id: str = Form(...),
db: Session = Depends(get_db)
):
"""
Rename a unit ID across all tables.
Updates the unit ID in roster, emitters, unit_history, and all foreign key references.
IMPORTANT: This operation updates the primary key, which affects all relationships.
"""
# Validate input
if not old_id or not new_id:
raise HTTPException(status_code=400, detail="Both old_id and new_id are required")
if old_id == new_id:
raise HTTPException(status_code=400, detail="New ID must be different from old ID")
# Check if old unit exists
old_unit = db.query(RosterUnit).filter(RosterUnit.id == old_id).first()
if not old_unit:
raise HTTPException(status_code=404, detail=f"Unit '{old_id}' not found")
# Check if new ID already exists
existing_unit = db.query(RosterUnit).filter(RosterUnit.id == new_id).first()
if existing_unit:
raise HTTPException(status_code=409, detail=f"Unit ID '{new_id}' already exists")
device_type = old_unit.device_type
try:
# Record history for the rename operation (using old_id since that's still valid)
record_history(
db=db,
unit_id=old_id,
change_type="id_change",
field_name="id",
old_value=old_id,
new_value=new_id,
source="manual",
notes=f"Unit renamed from '{old_id}' to '{new_id}'"
)
# Update roster table (primary)
old_unit.id = new_id
old_unit.last_updated = datetime.utcnow()
# Update emitters table
emitter = db.query(Emitter).filter(Emitter.id == old_id).first()
if emitter:
emitter.id = new_id
# Update unit_history table (all entries for this unit)
db.query(UnitHistory).filter(UnitHistory.unit_id == old_id).update(
{"unit_id": new_id},
synchronize_session=False
)
# Update deployed_with_modem_id references (units that reference this as modem)
db.query(RosterUnit).filter(RosterUnit.deployed_with_modem_id == old_id).update(
{"deployed_with_modem_id": new_id},
synchronize_session=False
)
# Update unit_assignments table (if exists)
try:
from backend.models import UnitAssignment
db.query(UnitAssignment).filter(UnitAssignment.unit_id == old_id).update(
{"unit_id": new_id},
synchronize_session=False
)
except Exception as e:
logger.warning(f"Could not update unit_assignments: {e}")
# Update recording_sessions table (if exists)
try:
from backend.models import RecordingSession
db.query(RecordingSession).filter(RecordingSession.unit_id == old_id).update(
{"unit_id": new_id},
synchronize_session=False
)
except Exception as e:
logger.warning(f"Could not update recording_sessions: {e}")
# Commit all changes
db.commit()
# If sound level meter, sync updated config to SLMM cache
if device_type == "sound_level_meter":
logger.info(f"Syncing renamed SLM {new_id} (was {old_id}) config to SLMM cache...")
result = await sync_slm_to_slmm_cache(
unit_id=new_id,
host=old_unit.slm_host,
tcp_port=old_unit.slm_tcp_port,
ftp_port=old_unit.slm_ftp_port,
deployed_with_modem_id=old_unit.deployed_with_modem_id,
db=db
)
if not result["success"]:
logger.warning(f"SLMM cache sync warning for renamed unit {new_id}: {result['message']}")
logger.info(f"Successfully renamed unit '{old_id}' to '{new_id}'")
return {
"success": True,
"message": f"Successfully renamed unit from '{old_id}' to '{new_id}'",
"old_id": old_id,
"new_id": new_id,
"device_type": device_type
}
except Exception as e:
db.rollback()
logger.error(f"Error renaming unit '{old_id}' to '{new_id}': {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to rename unit: {str(e)}"
)

View File

@@ -5,6 +5,7 @@ from typing import Dict, Any
from backend.database import get_db
from backend.services.snapshot import emit_status_snapshot
from backend.models import RosterUnit
router = APIRouter(prefix="/api", tags=["units"])
@@ -42,3 +43,32 @@ def get_unit_detail(unit_id: str, db: Session = Depends(get_db)):
"note": unit_data.get("note", ""),
"coordinates": coords
}
@router.get("/units/{unit_id}")
def get_unit_by_id(unit_id: str, db: Session = Depends(get_db)):
"""
Get unit data directly from the roster (for settings/configuration).
"""
unit = db.query(RosterUnit).filter_by(id=unit_id).first()
if not unit:
raise HTTPException(status_code=404, detail=f"Unit {unit_id} not found")
return {
"id": unit.id,
"unit_type": unit.unit_type,
"device_type": unit.device_type,
"deployed": unit.deployed,
"retired": unit.retired,
"note": unit.note,
"location": unit.location,
"address": unit.address,
"coordinates": unit.coordinates,
"slm_host": unit.slm_host,
"slm_tcp_port": unit.slm_tcp_port,
"slm_ftp_port": unit.slm_ftp_port,
"slm_model": unit.slm_model,
"slm_serial_number": unit.slm_serial_number,
"deployed_with_modem_id": unit.deployed_with_modem_id
}