from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel from sqlalchemy.orm import Session from typing import List, Optional from datetime import datetime, timedelta import json import logging from ..models.scheduled_job import ScheduledJob from ..services.postgresql_manager import PostgreSQLManager from ..utils.config import config from ..services.scheduler import scheduler router = APIRouter() logger = logging.getLogger(__name__) def get_db(): db_manager = PostgreSQLManager(config.DATABASE_URL) with db_manager.get_db_session() as session: yield session class ScheduledJobResponse(BaseModel): id: int job_type: str name: str interval_minutes: int enabled: bool last_run: Optional[datetime] next_run: Optional[datetime] params: Optional[str] class Config: from_attributes = True class JobUpdateRequest(BaseModel): interval_minutes: Optional[int] = None enabled: Optional[bool] = None params: Optional[dict] = None @router.get("/scheduling/jobs", response_model=List[ScheduledJobResponse]) def list_scheduled_jobs(db: Session = Depends(get_db)): """List all scheduled jobs.""" jobs = db.query(ScheduledJob).order_by(ScheduledJob.id).all() return jobs @router.put("/scheduling/jobs/{job_id}", response_model=ScheduledJobResponse) def update_scheduled_job(job_id: int, request: JobUpdateRequest, db: Session = Depends(get_db)): """Update a scheduled job's interval or enabled status.""" job = db.query(ScheduledJob).filter(ScheduledJob.id == job_id).first() if not job: raise HTTPException(status_code=404, detail="Job not found") if request.interval_minutes is not None: if request.interval_minutes < 1: raise HTTPException(status_code=400, detail="Interval must be at least 1 minute") job.interval_minutes = request.interval_minutes # If enabled, update next_run based on new interval if it's far in future? # Actually, standard behavior: next_run should be recalculated from last_run + new interval # OR just leave it. If we shorten it, we might want it to run sooner. # Let's recalculate next_run if it exists. if job.last_run: job.next_run = job.last_run + timedelta(minutes=job.interval_minutes) else: # If never run, next_run should be Now if enabled? # Or keep existing next_run? # If next_run is null and enabled, scheduler picks it up immediately. pass if request.enabled is not None: job.enabled = request.enabled if job.enabled and job.next_run is None: # If re-enabling and no next run, set to now job.next_run = datetime.now() if request.params is not None: job.params = json.dumps(request.params) db.commit() db.refresh(job) return job class JobCreateRequest(BaseModel): job_type: str name: str interval_minutes: int params: Optional[dict] = {} enabled: Optional[bool] = True @router.post("/scheduling/jobs", response_model=ScheduledJobResponse) def create_scheduled_job(request: JobCreateRequest, db: Session = Depends(get_db)): """Create a new scheduled job.""" # Validate job_type from ..services.scheduler import scheduler if request.job_type not in scheduler.TASK_MAP: raise HTTPException(status_code=400, detail=f"Invalid job_type. Must be one of: {list(scheduler.TASK_MAP.keys())}") new_job = ScheduledJob( job_type=request.job_type, name=request.name, interval_minutes=request.interval_minutes, params=json.dumps(request.params) if request.params else "{}", enabled=request.enabled, next_run=datetime.now() if request.enabled else None ) try: db.add(new_job) db.commit() db.refresh(new_job) return new_job except Exception as e: db.rollback() logger.error(f"Failed to create job: {e}") # Check for unique constraint on job_type if we enforced it? # The model has job_type unique=True. This might be a problem if we want multiple of same type? # User wants "new scheduled tasks" with "variables" -> implies multiple of same type (e.g. sync fitbit 10 days vs 30 days). # We need to remove unique=True from ScheduledJob model if it exists! raise HTTPException(status_code=400, detail=f"Failed to create job: {str(e)}") @router.delete("/scheduling/jobs/{job_id}", status_code=204) def delete_scheduled_job(job_id: int, db: Session = Depends(get_db)): """Delete a scheduled job.""" job = db.query(ScheduledJob).filter(ScheduledJob.id == job_id).first() if not job: raise HTTPException(status_code=404, detail="Job not found") db.delete(job) db.commit() return None