410 lines
12 KiB
Python
410 lines
12 KiB
Python
"""
|
|
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,
|
|
}
|