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:
2026-02-24 19:54:40 +00:00
parent da4e5f66c5
commit 7516bbea70
16 changed files with 509 additions and 123 deletions

View File

@@ -312,7 +312,7 @@ async def nrl_detail_page(
db: Session = Depends(get_db)
):
"""NRL (Noise Recording Location) detail page with tabs"""
from backend.models import Project, MonitoringLocation, UnitAssignment, RosterUnit, RecordingSession, DataFile
from backend.models import Project, MonitoringLocation, UnitAssignment, RosterUnit, MonitoringSession, DataFile
from sqlalchemy import and_
# Get project
@@ -348,23 +348,24 @@ async def nrl_detail_page(
assigned_unit = db.query(RosterUnit).filter_by(id=assignment.unit_id).first()
# Get session count
session_count = db.query(RecordingSession).filter_by(location_id=location_id).count()
session_count = db.query(MonitoringSession).filter_by(location_id=location_id).count()
# Get file count (DataFile links to session, not directly to location)
file_count = db.query(DataFile).join(
RecordingSession,
DataFile.session_id == RecordingSession.id
).filter(RecordingSession.location_id == location_id).count()
MonitoringSession,
DataFile.session_id == MonitoringSession.id
).filter(MonitoringSession.location_id == location_id).count()
# Check for active session
active_session = db.query(RecordingSession).filter(
active_session = db.query(MonitoringSession).filter(
and_(
RecordingSession.location_id == location_id,
RecordingSession.status == "recording"
MonitoringSession.location_id == location_id,
MonitoringSession.status == "recording"
)
).first()
return templates.TemplateResponse("nrl_detail.html", {
template = "vibration_location_detail.html" if location.location_type == "vibration" else "nrl_detail.html"
return templates.TemplateResponse(template, {
"request": request,
"project_id": project_id,
"location_id": location_id,