""" Scheduler Router Handles scheduled actions for automated recording control. """ from fastapi import APIRouter, Request, Depends, HTTPException, Query from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, JSONResponse from sqlalchemy.orm import Session from sqlalchemy import and_, or_ from datetime import datetime, timedelta from typing import Optional import uuid import json from backend.database import get_db from backend.models import ( Project, ScheduledAction, MonitoringLocation, UnitAssignment, RosterUnit, ) from backend.services.scheduler import get_scheduler router = APIRouter(prefix="/api/projects/{project_id}/scheduler", tags=["scheduler"]) templates = Jinja2Templates(directory="templates") # ============================================================================ # Scheduled Actions List # ============================================================================ @router.get("/actions", response_class=HTMLResponse) async def get_scheduled_actions( project_id: str, request: Request, db: Session = Depends(get_db), status: Optional[str] = Query(None), start_date: Optional[str] = Query(None), end_date: Optional[str] = Query(None), ): """ Get scheduled actions for a project. Returns HTML partial with agenda/calendar view. """ query = db.query(ScheduledAction).filter_by(project_id=project_id) # Filter by status if status: query = query.filter_by(execution_status=status) else: # By default, show pending and upcoming completed/failed query = query.filter( or_( ScheduledAction.execution_status == "pending", and_( ScheduledAction.execution_status.in_(["completed", "failed"]), ScheduledAction.scheduled_time >= datetime.utcnow() - timedelta(days=7), ), ) ) # Filter by date range if start_date: query = query.filter(ScheduledAction.scheduled_time >= datetime.fromisoformat(start_date)) if end_date: query = query.filter(ScheduledAction.scheduled_time <= datetime.fromisoformat(end_date)) actions = query.order_by(ScheduledAction.scheduled_time).all() # Enrich with location and unit details actions_data = [] for action in actions: location = db.query(MonitoringLocation).filter_by(id=action.location_id).first() unit = None if action.unit_id: unit = db.query(RosterUnit).filter_by(id=action.unit_id).first() else: # Get from assignment assignment = db.query(UnitAssignment).filter( and_( UnitAssignment.location_id == action.location_id, UnitAssignment.status == "active", ) ).first() if assignment: unit = db.query(RosterUnit).filter_by(id=assignment.unit_id).first() actions_data.append({ "action": action, "location": location, "unit": unit, }) return templates.TemplateResponse("partials/projects/scheduler_agenda.html", { "request": request, "project_id": project_id, "actions": actions_data, }) # ============================================================================ # Create Scheduled Action # ============================================================================ @router.post("/actions/create") async def create_scheduled_action( project_id: str, request: Request, db: Session = Depends(get_db), ): """ Create a new scheduled action. """ project = db.query(Project).filter_by(id=project_id).first() if not project: raise HTTPException(status_code=404, detail="Project not found") form_data = await request.form() location_id = form_data.get("location_id") location = db.query(MonitoringLocation).filter_by( id=location_id, project_id=project_id, ).first() if not location: raise HTTPException(status_code=404, detail="Location not found") # Determine device type from location device_type = "sound_level_meter" if location.location_type == "sound" else "seismograph" # Get unit_id (optional - can be determined from assignment at execution time) unit_id = form_data.get("unit_id") action = ScheduledAction( id=str(uuid.uuid4()), project_id=project_id, location_id=location_id, unit_id=unit_id, action_type=form_data.get("action_type"), device_type=device_type, scheduled_time=datetime.fromisoformat(form_data.get("scheduled_time")), execution_status="pending", notes=form_data.get("notes"), ) db.add(action) db.commit() db.refresh(action) return JSONResponse({ "success": True, "action_id": action.id, "message": f"Scheduled action '{action.action_type}' created for {action.scheduled_time}", }) # ============================================================================ # Schedule Recording Session # ============================================================================ @router.post("/schedule-session") async def schedule_recording_session( project_id: str, request: Request, db: Session = Depends(get_db), ): """ Schedule a complete recording session (start + stop). Creates two scheduled actions: start and stop. """ project = db.query(Project).filter_by(id=project_id).first() if not project: raise HTTPException(status_code=404, detail="Project not found") form_data = await request.form() location_id = form_data.get("location_id") location = db.query(MonitoringLocation).filter_by( id=location_id, project_id=project_id, ).first() if not location: raise HTTPException(status_code=404, detail="Location not found") device_type = "sound_level_meter" if location.location_type == "sound" else "seismograph" unit_id = form_data.get("unit_id") start_time = datetime.fromisoformat(form_data.get("start_time")) duration_minutes = int(form_data.get("duration_minutes", 60)) stop_time = start_time + timedelta(minutes=duration_minutes) # Create START action start_action = ScheduledAction( id=str(uuid.uuid4()), project_id=project_id, location_id=location_id, unit_id=unit_id, action_type="start", device_type=device_type, scheduled_time=start_time, execution_status="pending", notes=form_data.get("notes"), ) # Create STOP action stop_action = ScheduledAction( id=str(uuid.uuid4()), project_id=project_id, location_id=location_id, unit_id=unit_id, action_type="stop", device_type=device_type, scheduled_time=stop_time, execution_status="pending", notes=f"Auto-stop after {duration_minutes} minutes", ) db.add(start_action) db.add(stop_action) db.commit() return JSONResponse({ "success": True, "start_action_id": start_action.id, "stop_action_id": stop_action.id, "message": f"Recording session scheduled from {start_time} to {stop_time}", }) # ============================================================================ # Update/Cancel Scheduled Action # ============================================================================ @router.put("/actions/{action_id}") async def update_scheduled_action( project_id: str, action_id: str, request: Request, db: Session = Depends(get_db), ): """ Update a scheduled action (only if not yet executed). """ action = db.query(ScheduledAction).filter_by( id=action_id, project_id=project_id, ).first() if not action: raise HTTPException(status_code=404, detail="Action not found") if action.execution_status != "pending": raise HTTPException( status_code=400, detail="Cannot update action that has already been executed", ) data = await request.json() if "scheduled_time" in data: action.scheduled_time = datetime.fromisoformat(data["scheduled_time"]) if "notes" in data: action.notes = data["notes"] db.commit() return {"success": True, "message": "Action updated successfully"} @router.post("/actions/{action_id}/cancel") async def cancel_scheduled_action( project_id: str, action_id: str, db: Session = Depends(get_db), ): """ Cancel a pending scheduled action. """ action = db.query(ScheduledAction).filter_by( id=action_id, project_id=project_id, ).first() if not action: raise HTTPException(status_code=404, detail="Action not found") if action.execution_status != "pending": raise HTTPException( status_code=400, detail="Can only cancel pending actions", ) action.execution_status = "cancelled" db.commit() return {"success": True, "message": "Action cancelled successfully"} @router.delete("/actions/{action_id}") async def delete_scheduled_action( project_id: str, action_id: str, db: Session = Depends(get_db), ): """ Delete a scheduled action (only if pending or cancelled). """ action = db.query(ScheduledAction).filter_by( id=action_id, project_id=project_id, ).first() if not action: raise HTTPException(status_code=404, detail="Action not found") if action.execution_status not in ["pending", "cancelled"]: raise HTTPException( status_code=400, detail="Cannot delete action that has been executed", ) db.delete(action) db.commit() return {"success": True, "message": "Action deleted successfully"} # ============================================================================ # Manual Execution # ============================================================================ @router.post("/actions/{action_id}/execute") async def execute_action_now( project_id: str, action_id: str, db: Session = Depends(get_db), ): """ Manually trigger execution of a scheduled action (for testing/debugging). """ action = db.query(ScheduledAction).filter_by( id=action_id, project_id=project_id, ).first() if not action: raise HTTPException(status_code=404, detail="Action not found") if action.execution_status != "pending": raise HTTPException( status_code=400, detail="Action is not pending", ) # Execute via scheduler service scheduler = get_scheduler() result = await scheduler.execute_action_by_id(action_id) # Refresh from DB to get updated status db.refresh(action) return JSONResponse({ "success": result.get("success", False), "result": result, "action": { "id": action.id, "execution_status": action.execution_status, "executed_at": action.executed_at.isoformat() if action.executed_at else None, "error_message": action.error_message, }, }) # ============================================================================ # Scheduler Status # ============================================================================ @router.get("/status") async def get_scheduler_status(): """ Get scheduler service status. """ scheduler = get_scheduler() return { "running": scheduler.running, "check_interval": scheduler.check_interval, } @router.post("/execute-pending") async def trigger_pending_execution(): """ Manually trigger execution of all pending actions (for testing). """ scheduler = get_scheduler() results = await scheduler.execute_pending_actions() return { "success": True, "executed_count": len(results), "results": results, }