diff --git a/backend/main.py b/backend/main.py index 6ef6ca6..488430a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -233,8 +233,11 @@ async def nrl_detail_page( # 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() + # 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() # Check for active session active_session = db.query(RecordingSession).filter( diff --git a/backend/routers/project_locations.py b/backend/routers/project_locations.py index 8d63a0c..801e21a 100644 --- a/backend/routers/project_locations.py +++ b/backend/routers/project_locations.py @@ -460,8 +460,12 @@ async def get_nrl_files( """ from backend.models import DataFile, RecordingSession - files = db.query(DataFile).filter_by( - location_id=location_id + # Join DataFile with RecordingSession to filter by location_id + files = db.query(DataFile).join( + RecordingSession, + DataFile.session_id == RecordingSession.id + ).filter( + RecordingSession.location_id == location_id ).order_by(DataFile.created_at.desc()).all() # Enrich with session details diff --git a/backend/routers/projects.py b/backend/routers/projects.py index 7701982..5a72362 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -341,6 +341,230 @@ async def get_project_dashboard( }) +# ============================================================================ +# Project Types +# ============================================================================ + +@router.get("/{project_id}/header", response_class=JSONResponse) +async def get_project_header(project_id: str, db: Session = Depends(get_db)): + """ + Get project header information for dynamic display. + Returns JSON with project name, status, and type. + """ + project = db.query(Project).filter_by(id=project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + project_type = db.query(ProjectType).filter_by(id=project.project_type_id).first() + + return JSONResponse({ + "id": project.id, + "name": project.name, + "status": project.status, + "project_type_id": project.project_type_id, + "project_type_name": project_type.name if project_type else None, + }) + + +@router.get("/{project_id}/units", response_class=HTMLResponse) +async def get_project_units( + project_id: str, + request: Request, + db: Session = Depends(get_db), +): + """ + Get all units assigned to this project's locations. + Returns HTML partial with unit list. + """ + from backend.models import DataFile + + # Get all assignments for this project + assignments = db.query(UnitAssignment).filter( + and_( + UnitAssignment.project_id == project_id, + UnitAssignment.status == "active", + ) + ).all() + + # Enrich with unit and location details + units_data = [] + for assignment in assignments: + unit = db.query(RosterUnit).filter_by(id=assignment.unit_id).first() + 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( + 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 + ).filter( + RecordingSession.location_id == assignment.location_id, + RecordingSession.unit_id == assignment.unit_id, + ).scalar() + + # Check if currently recording + active_session = db.query(RecordingSession).filter( + and_( + RecordingSession.location_id == assignment.location_id, + RecordingSession.unit_id == assignment.unit_id, + RecordingSession.status == "recording", + ) + ).first() + + units_data.append({ + "assignment": assignment, + "unit": unit, + "location": location, + "session_count": session_count, + "file_count": file_count, + "active_session": active_session, + }) + + # Get project type for label context + project = db.query(Project).filter_by(id=project_id).first() + project_type = db.query(ProjectType).filter_by(id=project.project_type_id).first() if project else None + + return templates.TemplateResponse("partials/projects/unit_list.html", { + "request": request, + "project_id": project_id, + "units": units_data, + "project_type": project_type, + }) + + +@router.get("/{project_id}/schedules", response_class=HTMLResponse) +async def get_project_schedules( + project_id: str, + request: Request, + db: Session = Depends(get_db), + status: Optional[str] = Query(None), +): + """ + Get scheduled actions for this project. + Returns HTML partial with schedule list. + Optional status filter: pending, completed, failed, cancelled + """ + query = db.query(ScheduledAction).filter_by(project_id=project_id) + + # Filter by status if provided + if status: + query = query.filter(ScheduledAction.execution_status == status) + + schedules = query.order_by(ScheduledAction.scheduled_time.desc()).all() + + # Enrich with location details + schedules_data = [] + for schedule in schedules: + location = None + if schedule.location_id: + location = db.query(MonitoringLocation).filter_by(id=schedule.location_id).first() + + schedules_data.append({ + "schedule": schedule, + "location": location, + }) + + return templates.TemplateResponse("partials/projects/schedule_list.html", { + "request": request, + "project_id": project_id, + "schedules": schedules_data, + }) + + +@router.get("/{project_id}/sessions", response_class=HTMLResponse) +async def get_project_sessions( + project_id: str, + request: Request, + db: Session = Depends(get_db), + status: Optional[str] = Query(None), +): + """ + Get all recording sessions for this project. + Returns HTML partial with session list. + Optional status filter: recording, completed, paused, failed + """ + query = db.query(RecordingSession).filter_by(project_id=project_id) + + # Filter by status if provided + if status: + query = query.filter(RecordingSession.status == status) + + sessions = query.order_by(RecordingSession.started_at.desc()).all() + + # Enrich with unit and location details + sessions_data = [] + for session in sessions: + unit = None + location = None + + if session.unit_id: + unit = db.query(RosterUnit).filter_by(id=session.unit_id).first() + if session.location_id: + location = db.query(MonitoringLocation).filter_by(id=session.location_id).first() + + sessions_data.append({ + "session": session, + "unit": unit, + "location": location, + }) + + return templates.TemplateResponse("partials/projects/session_list.html", { + "request": request, + "project_id": project_id, + "sessions": sessions_data, + }) + + +@router.get("/{project_id}/files", response_class=HTMLResponse) +async def get_project_files( + project_id: str, + request: Request, + db: Session = Depends(get_db), + file_type: Optional[str] = Query(None), +): + """ + Get all data files from all sessions in this project. + Returns HTML partial with file list. + Optional file_type filter: audio, data, log, etc. + """ + from backend.models import DataFile + + # Join through RecordingSession to get project files + query = db.query(DataFile).join( + RecordingSession, + DataFile.session_id == RecordingSession.id + ).filter(RecordingSession.project_id == project_id) + + # Filter by file type if provided + if file_type: + query = query.filter(DataFile.file_type == file_type) + + files = query.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, + "files": files_data, + }) + + # ============================================================================ # Project Types # ============================================================================ diff --git a/templates/partials/projects/file_list.html b/templates/partials/projects/file_list.html index f346e42..103a094 100644 --- a/templates/partials/projects/file_list.html +++ b/templates/partials/projects/file_list.html @@ -34,7 +34,7 @@
- {{ item.file.file_name }} + {{ item.file.file_path.split('/')[-1] if item.file.file_path else 'Unknown' }}
{% if item.file.file_path %}
diff --git a/templates/partials/projects/schedule_list.html b/templates/partials/projects/schedule_list.html new file mode 100644 index 0000000..5054d6e --- /dev/null +++ b/templates/partials/projects/schedule_list.html @@ -0,0 +1,149 @@ + +{% if schedules %} +
+ {% for item in schedules %} +
+
+
+
+

+ {{ item.schedule.action_type }} +

+ {% if item.schedule.execution_status == 'pending' %} + + Pending + + {% elif item.schedule.execution_status == 'completed' %} + + Completed + + {% elif item.schedule.execution_status == 'failed' %} + + Failed + + {% elif item.schedule.execution_status == 'cancelled' %} + + Cancelled + + {% endif %} +
+ +
+ {% if item.location %} + + {% endif %} + +
+ Scheduled: + {{ item.schedule.scheduled_time.strftime('%Y-%m-%d %H:%M') if item.schedule.scheduled_time else 'N/A' }} +
+ + {% if item.schedule.executed_at %} +
+ Executed: + {{ item.schedule.executed_at.strftime('%Y-%m-%d %H:%M') }} +
+ {% endif %} + + {% if item.schedule.created_at %} +
+ Created: + {{ item.schedule.created_at.strftime('%Y-%m-%d %H:%M') }} +
+ {% endif %} +
+ + {% if item.schedule.description %} +

+ {{ item.schedule.description }} +

+ {% endif %} + + {% if item.schedule.result_message %} +
+ Result: + {{ item.schedule.result_message }} +
+ {% endif %} +
+ +
+ {% if item.schedule.execution_status == 'pending' %} + + + {% endif %} + +
+
+
+ {% endfor %} +
+{% else %} +
+ + + +

No scheduled actions yet

+

Create schedules to automate tasks

+
+{% endif %} + + diff --git a/templates/partials/projects/session_list.html b/templates/partials/projects/session_list.html index 3e55067..ef8cad5 100644 --- a/templates/partials/projects/session_list.html +++ b/templates/partials/projects/session_list.html @@ -44,10 +44,10 @@ {{ item.session.started_at.strftime('%Y-%m-%d %H:%M') if item.session.started_at else 'N/A' }}
- {% if item.session.ended_at %} + {% if item.session.stopped_at %}
Ended: - {{ item.session.ended_at.strftime('%Y-%m-%d %H:%M') }} + {{ item.session.stopped_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %} diff --git a/templates/partials/projects/unit_list.html b/templates/partials/projects/unit_list.html new file mode 100644 index 0000000..f8837e9 --- /dev/null +++ b/templates/partials/projects/unit_list.html @@ -0,0 +1,99 @@ + +{% if units %} +
+ {% for item in units %} +
+
+
+
+

+ + {{ item.unit.id }} + +

+ {% if item.active_session %} + + + Recording + + {% else %} + + Available + + {% endif %} +
+ +
+ {% if item.location %} +
+ + {% if project_type and project_type.id == 'sound_monitoring' %} + NRL: + {% else %} + Location: + {% endif %} + + + {{ item.location.name }} + +
+ {% endif %} + + {% if item.unit.slm_model %} +
+ Model: + {{ item.unit.slm_model }} +
+ {% endif %} + +
+ Sessions: + {{ item.session_count }} +
+ +
+ Files: + {{ item.file_count }} +
+ + {% if item.assignment.assigned_at %} +
+ Assigned: + {{ item.assignment.assigned_at.strftime('%Y-%m-%d %H:%M') }} +
+ {% endif %} +
+ + {% if item.unit.note %} +

+ {{ item.unit.note }} +

+ {% endif %} +
+ +
+ + View Unit + + {% if item.location %} + + View NRL + + {% endif %} +
+
+
+ {% endfor %} +
+{% else %} +
+ + + +

No units assigned yet

+

Assign units to locations to get started

+
+{% endif %} diff --git a/templates/projects/detail.html b/templates/projects/detail.html index 9d0b200..efce1b2 100644 --- a/templates/projects/detail.html +++ b/templates/projects/detail.html @@ -3,113 +3,308 @@ {% block title %}Project Dashboard - Terra-View{% endblock %} {% block content %} -
-
-

Project Dashboard

-

Sound monitoring project overview and assignments

+ +
+ +
+ + +
+
+
+
-
-
+ + +
+
+ + + + + + +
-
-
-
-
-
-
-
-
-
-
- - -