diff --git a/backend/main.py b/backend/main.py index 0312ab7..6ef6ca6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -157,6 +157,15 @@ async def sound_level_meters_page(request: Request): return templates.TemplateResponse("sound_level_meters.html", {"request": request}) +@app.get("/slm/{unit_id}", response_class=HTMLResponse) +async def slm_legacy_dashboard(request: Request, unit_id: str): + """Legacy SLM control center dashboard for a specific unit""" + return templates.TemplateResponse("slm_legacy_dashboard.html", { + "request": request, + "unit_id": unit_id + }) + + @app.get("/seismographs", response_class=HTMLResponse) async def seismographs_page(request: Request): """Seismographs management dashboard""" @@ -178,6 +187,77 @@ async def project_detail_page(request: Request, project_id: str): }) +@app.get("/projects/{project_id}/nrl/{location_id}", response_class=HTMLResponse) +async def nrl_detail_page( + request: Request, + project_id: str, + location_id: str, + db: Session = Depends(get_db) +): + """NRL (Noise Recording Location) detail page with tabs""" + from backend.models import Project, MonitoringLocation, UnitAssignment, RosterUnit, RecordingSession, DataFile + from sqlalchemy import and_ + + # Get project + project = db.query(Project).filter_by(id=project_id).first() + if not project: + return templates.TemplateResponse("404.html", { + "request": request, + "message": "Project not found" + }, status_code=404) + + # Get location + location = db.query(MonitoringLocation).filter_by( + id=location_id, + project_id=project_id + ).first() + + if not location: + return templates.TemplateResponse("404.html", { + "request": request, + "message": "Location not found" + }, status_code=404) + + # Get active assignment + assignment = db.query(UnitAssignment).filter( + and_( + UnitAssignment.location_id == location_id, + UnitAssignment.status == "active" + ) + ).first() + + assigned_unit = None + if assignment: + 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() + + # Get file count + file_count = db.query(DataFile).filter_by(location_id=location_id).count() + + # Check for active session + active_session = db.query(RecordingSession).filter( + and_( + RecordingSession.location_id == location_id, + RecordingSession.status == "recording" + ) + ).first() + + return templates.TemplateResponse("nrl_detail.html", { + "request": request, + "project_id": project_id, + "location_id": location_id, + "project": project, + "location": location, + "assignment": assignment, + "assigned_unit": assigned_unit, + "session_count": session_count, + "file_count": file_count, + "active_session": active_session, + }) + + # ===== PWA ROUTES ===== @app.get("/sw.js") diff --git a/backend/routers/project_locations.py b/backend/routers/project_locations.py index 37312e2..8d63a0c 100644 --- a/backend/routers/project_locations.py +++ b/backend/routers/project_locations.py @@ -404,3 +404,81 @@ async def get_available_units( ] return available_units + + +# ============================================================================ +# NRL-specific endpoints for detail page +# ============================================================================ + +@router.get("/nrl/{location_id}/sessions", response_class=HTMLResponse) +async def get_nrl_sessions( + project_id: str, + location_id: str, + request: Request, + db: Session = Depends(get_db), +): + """ + Get recording sessions for a specific NRL. + Returns HTML partial with session list. + """ + from backend.models import RecordingSession, RosterUnit + + sessions = db.query(RecordingSession).filter_by( + location_id=location_id + ).order_by(RecordingSession.started_at.desc()).all() + + # Enrich with unit details + sessions_data = [] + for session in sessions: + unit = None + if session.unit_id: + unit = db.query(RosterUnit).filter_by(id=session.unit_id).first() + + sessions_data.append({ + "session": session, + "unit": unit, + }) + + return templates.TemplateResponse("partials/projects/session_list.html", { + "request": request, + "project_id": project_id, + "location_id": location_id, + "sessions": sessions_data, + }) + + +@router.get("/nrl/{location_id}/files", response_class=HTMLResponse) +async def get_nrl_files( + project_id: str, + location_id: str, + request: Request, + db: Session = Depends(get_db), +): + """ + Get data files for a specific NRL. + Returns HTML partial with file list. + """ + from backend.models import DataFile, RecordingSession + + files = db.query(DataFile).filter_by( + location_id=location_id + ).order_by(DataFile.created_at.desc()).all() + + # Enrich with session details + files_data = [] + for file in files: + session = None + if file.session_id: + session = db.query(RecordingSession).filter_by(id=file.session_id).first() + + files_data.append({ + "file": file, + "session": session, + }) + + return templates.TemplateResponse("partials/projects/file_list.html", { + "request": request, + "project_id": project_id, + "location_id": location_id, + "files": files_data, + }) diff --git a/docker-compose.yml b/docker-compose.yml index bda2c14..5984715 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,26 +23,26 @@ services: start_period: 40s # --- TERRA-VIEW DEVELOPMENT --- - terra-view-dev: - build: . - container_name: terra-view-dev - ports: - - "1001:8001" - volumes: - - ./data-dev:/app/data - environment: - - PYTHONUNBUFFERED=1 - - ENVIRONMENT=development - - SLMM_BASE_URL=http://slmm:8100 - restart: unless-stopped - depends_on: - - slmm - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8001/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s + # terra-view-dev: + # build: . + # container_name: terra-view-dev + # ports: + # - "1001:8001" + # volumes: + # - ./data-dev:/app/data + # environment: + # - PYTHONUNBUFFERED=1 + # - ENVIRONMENT=development + # - SLMM_BASE_URL=http://slmm:8100 + # restart: unless-stopped + # depends_on: + # - slmm + # healthcheck: + # test: ["CMD", "curl", "-f", "http://localhost:8001/health"] + # interval: 30s + # timeout: 10s + # retries: 3 + # start_period: 40s # --- SLMM (Sound Level Meter Manager) --- slmm: diff --git a/templates/nrl_detail.html b/templates/nrl_detail.html new file mode 100644 index 0000000..1ccd8cd --- /dev/null +++ b/templates/nrl_detail.html @@ -0,0 +1,563 @@ +{% extends "base.html" %} + +{% block title %}{{ location.name }} - NRL Detail{% endblock %} + +{% block content %} + +
+ +
+ + +
+
+
+

+ + + + + {{ location.name }} +

+

+ Noise Recording Location • {{ project.name }} +

+
+
+ {% if assigned_unit %} + + + + + Unit Assigned + + {% else %} + + No Unit Assigned + + {% endif %} +
+
+
+ + +
+ +
+ + +
+ +
+
+ +
+

Location Details

+
+
+
Name
+
{{ location.name }}
+
+ {% if location.description %} +
+
Description
+
{{ location.description }}
+
+ {% endif %} + {% if location.address %} +
+
Address
+
{{ location.address }}
+
+ {% endif %} + {% if location.coordinates %} +
+
Coordinates
+
{{ location.coordinates }}
+
+ {% endif %} +
+
Created
+
{{ location.created_at.strftime('%Y-%m-%d %H:%M') if location.created_at else 'N/A' }}
+
+
+
+ + +
+

Unit Assignment

+ {% if assigned_unit %} +
+
+
Assigned Unit
+ +
+ {% if assigned_unit.slm_model %} +
+
Model
+
{{ assigned_unit.slm_model }}
+
+ {% endif %} + {% if assignment %} +
+
Assigned Since
+
{{ assignment.assigned_at.strftime('%Y-%m-%d %H:%M') if assignment.assigned_at else 'N/A' }}
+
+ {% if assignment.notes %} +
+
Notes
+
{{ assignment.notes }}
+
+ {% endif %} + {% endif %} +
+ +
+
+ {% else %} +
+ + + +

No unit currently assigned

+ +
+ {% endif %} +
+
+ + +
+
+
+
+

Total Sessions

+

{{ session_count }}

+
+
+ + + +
+
+
+ +
+
+
+

Data Files

+

{{ file_count }}

+
+
+ + + +
+
+
+ +
+
+
+

Active Session

+

+ {% if active_session %} + Recording + {% else %} + Idle + {% endif %} +

+
+
+ + + +
+
+
+
+
+ + + + + + {% if assigned_unit %} + + {% endif %} + + + + + + +
+ + + + + +{% endblock %} diff --git a/templates/partials/projects/file_list.html b/templates/partials/projects/file_list.html new file mode 100644 index 0000000..f346e42 --- /dev/null +++ b/templates/partials/projects/file_list.html @@ -0,0 +1,126 @@ + +{% if files %} +
+ + + + + + + + + + + + + {% for item in files %} + + + + + + + + + {% endfor %} + +
+ File Name + + Type + + Size + + Created + + Session + + Actions +
+
+ + + +
+
+ {{ item.file.file_name }} +
+ {% if item.file.file_path %} +
+ {{ item.file.file_path }} +
+ {% endif %} +
+
+
+ + {{ item.file.file_type or 'unknown' }} + + + {% if item.file.file_size_bytes %} + {% if item.file.file_size_bytes < 1024 %} + {{ item.file.file_size_bytes }} B + {% elif item.file.file_size_bytes < 1048576 %} + {{ "%.1f"|format(item.file.file_size_bytes / 1024) }} KB + {% elif item.file.file_size_bytes < 1073741824 %} + {{ "%.1f"|format(item.file.file_size_bytes / 1048576) }} MB + {% else %} + {{ "%.2f"|format(item.file.file_size_bytes / 1073741824) }} GB + {% endif %} + {% else %} + - + {% endif %} + + {{ item.file.created_at.strftime('%Y-%m-%d %H:%M') if item.file.created_at else 'N/A' }} + + {% if item.session %} + + {{ item.session.id[:8] }}... + + {% else %} + - + {% endif %} + +
+ + +
+
+
+{% else %} +
+ + + +

No data files yet

+

Files will appear here after recording sessions

+
+{% endif %} + + diff --git a/templates/partials/projects/location_list.html b/templates/partials/projects/location_list.html index dfb3ef8..c455927 100644 --- a/templates/partials/projects/location_list.html +++ b/templates/partials/projects/location_list.html @@ -2,11 +2,14 @@ {% if locations %}
{% for item in locations %} -
+
-
+
-

{{ item.location.name }}

+ + {{ item.location.name }} + {% if item.location.location_type %} {{ item.location.location_type|capitalize }} diff --git a/templates/partials/projects/session_list.html b/templates/partials/projects/session_list.html new file mode 100644 index 0000000..3e55067 --- /dev/null +++ b/templates/partials/projects/session_list.html @@ -0,0 +1,107 @@ + +{% if sessions %} +
+ {% for item in sessions %} +
+
+
+
+

+ Session {{ item.session.id[:8] }}... +

+ {% if item.session.status == 'recording' %} + + + Recording + + {% elif item.session.status == 'completed' %} + + Completed + + {% elif item.session.status == 'paused' %} + + Paused + + {% elif item.session.status == 'failed' %} + + Failed + + {% endif %} +
+ +
+ {% if item.unit %} + + {% endif %} + +
+ Started: + {{ item.session.started_at.strftime('%Y-%m-%d %H:%M') if item.session.started_at else 'N/A' }} +
+ + {% if item.session.ended_at %} +
+ Ended: + {{ item.session.ended_at.strftime('%Y-%m-%d %H:%M') }} +
+ {% endif %} + + {% if item.session.duration_seconds %} +
+ Duration: + {{ (item.session.duration_seconds // 3600) }}h {{ ((item.session.duration_seconds % 3600) // 60) }}m +
+ {% endif %} +
+ + {% if item.session.notes %} +

+ {{ item.session.notes }} +

+ {% endif %} +
+ +
+ {% if item.session.status == 'recording' %} + + {% endif %} + +
+
+
+ {% endfor %} +
+{% else %} +
+ + + +

No recording sessions yet

+

Schedule a session to get started

+
+{% endif %} + + diff --git a/templates/slm_legacy_dashboard.html b/templates/slm_legacy_dashboard.html new file mode 100644 index 0000000..45c1c43 --- /dev/null +++ b/templates/slm_legacy_dashboard.html @@ -0,0 +1,132 @@ +{% extends "base.html" %} + +{% block title %}{{ unit_id }} - Sound Level Meter Control Center{% endblock %} + +{% block content %} + +
+ +
+ + +
+
+
+

+ + + + {{ unit_id }} +

+

+ Sound Level Meter Control Center +

+
+
+ +
+
+
+ + +
+
+ +
+
+

Loading control center...

+
+
+
+ + + + + + +{% endblock %}