feat: add manual SD card data upload for offline NRLs; rename RecordingSession to MonitoringSession
- Add POST /api/projects/{project_id}/nrl/{location_id}/upload-data endpoint
accepting a ZIP or multi-file select of .rnd/.rnh files from an SD card.
Parses .rnh metadata for session start/stop times, serial number, and store
name. Creates a MonitoringSession (no unit assignment required) and DataFile
records for each measurement file.
- Add Upload Data button and collapsible upload panel to the NRL detail Data
Files tab, with inline success/error feedback and automatic file list refresh
via HTMX after import.
- Rename RecordingSession -> MonitoringSession throughout the codebase
(models.py, projects.py, project_locations.py, scheduler.py, roster_rename.py,
main.py, init_projects_db.py, scripts/rename_unit.py). DB table renamed from
recording_sessions to monitoring_sessions; old indexes dropped and recreated.
- Update all template UI copy from Recording Sessions to Monitoring Sessions
(nrl_detail, projects/detail, session_list, schedule_oneoff, roster).
- Add backend/migrate_rename_recording_to_monitoring_sessions.py for applying
the table rename on production databases before deploying this build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ from backend.models import (
|
||||
ProjectType,
|
||||
MonitoringLocation,
|
||||
UnitAssignment,
|
||||
RecordingSession,
|
||||
MonitoringSession,
|
||||
ScheduledAction,
|
||||
RecurringSchedule,
|
||||
RosterUnit,
|
||||
@@ -89,10 +89,10 @@ async def get_projects_list(
|
||||
).scalar()
|
||||
|
||||
# Count active sessions
|
||||
active_session_count = db.query(func.count(RecordingSession.id)).filter(
|
||||
active_session_count = db.query(func.count(MonitoringSession.id)).filter(
|
||||
and_(
|
||||
RecordingSession.project_id == project.id,
|
||||
RecordingSession.status == "recording",
|
||||
MonitoringSession.project_id == project.id,
|
||||
MonitoringSession.status == "recording",
|
||||
)
|
||||
).scalar()
|
||||
|
||||
@@ -135,7 +135,7 @@ async def get_projects_stats(request: Request, db: Session = Depends(get_db)):
|
||||
).scalar()
|
||||
|
||||
# Count active recording sessions
|
||||
active_sessions = db.query(func.count(RecordingSession.id)).filter_by(
|
||||
active_sessions = db.query(func.count(MonitoringSession.id)).filter_by(
|
||||
status="recording"
|
||||
).scalar()
|
||||
|
||||
@@ -410,7 +410,7 @@ async def permanently_delete_project(project_id: str, db: Session = Depends(get_
|
||||
# Delete related data
|
||||
db.query(RecurringSchedule).filter_by(project_id=project_id).delete()
|
||||
db.query(ScheduledAction).filter_by(project_id=project_id).delete()
|
||||
db.query(RecordingSession).filter_by(project_id=project_id).delete()
|
||||
db.query(MonitoringSession).filter_by(project_id=project_id).delete()
|
||||
db.query(UnitAssignment).filter_by(project_id=project_id).delete()
|
||||
db.query(MonitoringLocation).filter_by(project_id=project_id).delete()
|
||||
db.delete(project)
|
||||
@@ -501,18 +501,18 @@ async def get_project_dashboard(
|
||||
})
|
||||
|
||||
# Get active recording sessions
|
||||
active_sessions = db.query(RecordingSession).filter(
|
||||
active_sessions = db.query(MonitoringSession).filter(
|
||||
and_(
|
||||
RecordingSession.project_id == project_id,
|
||||
RecordingSession.status == "recording",
|
||||
MonitoringSession.project_id == project_id,
|
||||
MonitoringSession.status == "recording",
|
||||
)
|
||||
).all()
|
||||
|
||||
# Get completed sessions count
|
||||
completed_sessions_count = db.query(func.count(RecordingSession.id)).filter(
|
||||
completed_sessions_count = db.query(func.count(MonitoringSession.id)).filter(
|
||||
and_(
|
||||
RecordingSession.project_id == project_id,
|
||||
RecordingSession.status == "completed",
|
||||
MonitoringSession.project_id == project_id,
|
||||
MonitoringSession.status == "completed",
|
||||
)
|
||||
).scalar()
|
||||
|
||||
@@ -591,26 +591,26 @@ async def get_project_units(
|
||||
location = db.query(MonitoringLocation).filter_by(id=assignment.location_id).first()
|
||||
|
||||
# Count sessions for this assignment
|
||||
session_count = db.query(func.count(RecordingSession.id)).filter_by(
|
||||
session_count = db.query(func.count(MonitoringSession.id)).filter_by(
|
||||
location_id=assignment.location_id,
|
||||
unit_id=assignment.unit_id,
|
||||
).scalar()
|
||||
|
||||
# Count files from sessions
|
||||
file_count = db.query(func.count(DataFile.id)).join(
|
||||
RecordingSession,
|
||||
DataFile.session_id == RecordingSession.id
|
||||
MonitoringSession,
|
||||
DataFile.session_id == MonitoringSession.id
|
||||
).filter(
|
||||
RecordingSession.location_id == assignment.location_id,
|
||||
RecordingSession.unit_id == assignment.unit_id,
|
||||
MonitoringSession.location_id == assignment.location_id,
|
||||
MonitoringSession.unit_id == assignment.unit_id,
|
||||
).scalar()
|
||||
|
||||
# Check if currently recording
|
||||
active_session = db.query(RecordingSession).filter(
|
||||
active_session = db.query(MonitoringSession).filter(
|
||||
and_(
|
||||
RecordingSession.location_id == assignment.location_id,
|
||||
RecordingSession.unit_id == assignment.unit_id,
|
||||
RecordingSession.status == "recording",
|
||||
MonitoringSession.location_id == assignment.location_id,
|
||||
MonitoringSession.unit_id == assignment.unit_id,
|
||||
MonitoringSession.status == "recording",
|
||||
)
|
||||
).first()
|
||||
|
||||
@@ -797,13 +797,13 @@ async def get_project_sessions(
|
||||
Returns HTML partial with session list.
|
||||
Optional status filter: recording, completed, paused, failed
|
||||
"""
|
||||
query = db.query(RecordingSession).filter_by(project_id=project_id)
|
||||
query = db.query(MonitoringSession).filter_by(project_id=project_id)
|
||||
|
||||
# Filter by status if provided
|
||||
if status:
|
||||
query = query.filter(RecordingSession.status == status)
|
||||
query = query.filter(MonitoringSession.status == status)
|
||||
|
||||
sessions = query.order_by(RecordingSession.started_at.desc()).all()
|
||||
sessions = query.order_by(MonitoringSession.started_at.desc()).all()
|
||||
|
||||
# Enrich with unit and location details
|
||||
sessions_data = []
|
||||
@@ -895,18 +895,18 @@ async def ftp_download_to_server(
|
||||
raise HTTPException(status_code=400, detail="Missing unit_id or remote_path")
|
||||
|
||||
# Get or create active session for this location/unit
|
||||
session = db.query(RecordingSession).filter(
|
||||
session = db.query(MonitoringSession).filter(
|
||||
and_(
|
||||
RecordingSession.project_id == project_id,
|
||||
RecordingSession.location_id == location_id,
|
||||
RecordingSession.unit_id == unit_id,
|
||||
RecordingSession.status.in_(["recording", "paused"])
|
||||
MonitoringSession.project_id == project_id,
|
||||
MonitoringSession.location_id == location_id,
|
||||
MonitoringSession.unit_id == unit_id,
|
||||
MonitoringSession.status.in_(["recording", "paused"])
|
||||
)
|
||||
).first()
|
||||
|
||||
# If no active session, create one
|
||||
if not session:
|
||||
session = RecordingSession(
|
||||
session = MonitoringSession(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=project_id,
|
||||
location_id=location_id,
|
||||
@@ -1060,18 +1060,18 @@ async def ftp_download_folder_to_server(
|
||||
raise HTTPException(status_code=400, detail="Missing unit_id or remote_path")
|
||||
|
||||
# Get or create active session for this location/unit
|
||||
session = db.query(RecordingSession).filter(
|
||||
session = db.query(MonitoringSession).filter(
|
||||
and_(
|
||||
RecordingSession.project_id == project_id,
|
||||
RecordingSession.location_id == location_id,
|
||||
RecordingSession.unit_id == unit_id,
|
||||
RecordingSession.status.in_(["recording", "paused"])
|
||||
MonitoringSession.project_id == project_id,
|
||||
MonitoringSession.location_id == location_id,
|
||||
MonitoringSession.unit_id == unit_id,
|
||||
MonitoringSession.status.in_(["recording", "paused"])
|
||||
)
|
||||
).first()
|
||||
|
||||
# If no active session, create one
|
||||
if not session:
|
||||
session = RecordingSession(
|
||||
session = MonitoringSession(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=project_id,
|
||||
location_id=location_id,
|
||||
@@ -1231,9 +1231,9 @@ async def get_unified_files(
|
||||
import json
|
||||
|
||||
# Get all sessions for this project
|
||||
sessions = db.query(RecordingSession).filter_by(
|
||||
sessions = db.query(MonitoringSession).filter_by(
|
||||
project_id=project_id
|
||||
).order_by(RecordingSession.started_at.desc()).all()
|
||||
).order_by(MonitoringSession.started_at.desc()).all()
|
||||
|
||||
sessions_data = []
|
||||
for session in sessions:
|
||||
@@ -1310,7 +1310,7 @@ async def download_project_file(
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Verify file belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
|
||||
session = db.query(MonitoringSession).filter_by(id=file_record.session_id).first()
|
||||
if not session or session.project_id != project_id:
|
||||
raise HTTPException(status_code=403, detail="File does not belong to this project")
|
||||
|
||||
@@ -1344,7 +1344,7 @@ async def download_session_files(
|
||||
import zipfile
|
||||
|
||||
# Verify session belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=session_id).first()
|
||||
session = db.query(MonitoringSession).filter_by(id=session_id).first()
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
if session.project_id != project_id:
|
||||
@@ -1412,7 +1412,7 @@ async def delete_project_file(
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Verify file belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
|
||||
session = db.query(MonitoringSession).filter_by(id=file_record.session_id).first()
|
||||
if not session or session.project_id != project_id:
|
||||
raise HTTPException(status_code=403, detail="File does not belong to this project")
|
||||
|
||||
@@ -1442,7 +1442,7 @@ async def delete_session(
|
||||
from pathlib import Path
|
||||
|
||||
# Verify session belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=session_id).first()
|
||||
session = db.query(MonitoringSession).filter_by(id=session_id).first()
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
if session.project_id != project_id:
|
||||
@@ -1491,7 +1491,7 @@ async def view_rnd_file(
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Verify file belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
|
||||
session = db.query(MonitoringSession).filter_by(id=file_record.session_id).first()
|
||||
if not session or session.project_id != project_id:
|
||||
raise HTTPException(status_code=403, detail="File does not belong to this project")
|
||||
|
||||
@@ -1557,7 +1557,7 @@ async def get_rnd_data(
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Verify file belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
|
||||
session = db.query(MonitoringSession).filter_by(id=file_record.session_id).first()
|
||||
if not session or session.project_id != project_id:
|
||||
raise HTTPException(status_code=403, detail="File does not belong to this project")
|
||||
|
||||
@@ -1695,7 +1695,7 @@ async def generate_excel_report(
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Verify file belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
|
||||
session = db.query(MonitoringSession).filter_by(id=file_record.session_id).first()
|
||||
if not session or session.project_id != project_id:
|
||||
raise HTTPException(status_code=403, detail="File does not belong to this project")
|
||||
|
||||
@@ -2101,7 +2101,7 @@ async def preview_report_data(
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Verify file belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
|
||||
session = db.query(MonitoringSession).filter_by(id=file_record.session_id).first()
|
||||
if not session or session.project_id != project_id:
|
||||
raise HTTPException(status_code=403, detail="File does not belong to this project")
|
||||
|
||||
@@ -2309,7 +2309,7 @@ async def generate_report_from_preview(
|
||||
if not file_record:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
|
||||
session = db.query(MonitoringSession).filter_by(id=file_record.session_id).first()
|
||||
if not session or session.project_id != project_id:
|
||||
raise HTTPException(status_code=403, detail="File does not belong to this project")
|
||||
|
||||
@@ -2471,7 +2471,7 @@ async def generate_combined_excel_report(
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
# Get all sessions with measurement files
|
||||
sessions = db.query(RecordingSession).filter_by(project_id=project_id).all()
|
||||
sessions = db.query(MonitoringSession).filter_by(project_id=project_id).all()
|
||||
|
||||
# Collect all Leq RND files grouped by location
|
||||
# Only include files with '_Leq_' in the path (15-minute averaged data)
|
||||
|
||||
Reference in New Issue
Block a user