from typing import List from datetime import datetime from app.core.utils import now_tw from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from sqlalchemy.orm import Session from croniter import croniter from app.core.database import get_db from app.models.schedule import Schedule from app.schemas.schedule import ( ScheduleResponse, ScheduleUpdate, ScheduleLogResponse, LogResultsResponse, TenantResultItem, AccountResultItem, ) router = APIRouter(prefix="/schedules", tags=["schedules"]) @router.get("", response_model=List[ScheduleResponse]) def list_schedules(db: Session = Depends(get_db)): return db.query(Schedule).order_by(Schedule.id).all() @router.get("/{schedule_id}", response_model=ScheduleResponse) def get_schedule(schedule_id: int, db: Session = Depends(get_db)): s = db.get(Schedule, schedule_id) if not s: raise HTTPException(status_code=404, detail="Schedule not found") return s @router.put("/{schedule_id}", response_model=ScheduleResponse) def update_schedule_cron(schedule_id: int, payload: ScheduleUpdate, db: Session = Depends(get_db)): s = db.get(Schedule, schedule_id) if not s: raise HTTPException(status_code=404, detail="Schedule not found") # Validate cron expression (5-field: 分 時 日 月 週) try: cron = croniter(payload.cron_timer, now_tw()) next_run = cron.get_next(datetime) except Exception: raise HTTPException(status_code=422, detail="Invalid cron expression") s.cron_timer = payload.cron_timer s.next_run_at = next_run db.commit() db.refresh(s) return s @router.post("/{schedule_id}/run", status_code=202) def manual_run(schedule_id: int, background_tasks: BackgroundTasks, db: Session = Depends(get_db)): """手動觸發排程(非同步執行)""" s = db.get(Schedule, schedule_id) if not s: raise HTTPException(status_code=404, detail="Schedule not found") if s.status == "Going": raise HTTPException(status_code=409, detail="Schedule is already running") from app.services.scheduler.runner import dispatch_schedule background_tasks.add_task(dispatch_schedule, schedule_id) return {"message": f"Schedule '{s.name}' triggered", "schedule_id": schedule_id} @router.get("/{schedule_id}/logs", response_model=List[ScheduleLogResponse]) def get_schedule_logs(schedule_id: int, limit: int = 20, db: Session = Depends(get_db)): from app.models.schedule import ScheduleLog logs = ( db.query(ScheduleLog) .filter(ScheduleLog.schedule_id == schedule_id) .order_by(ScheduleLog.started_at.desc()) .limit(limit) .all() ) return logs @router.get("/{schedule_id}/logs/{log_id}/results", response_model=LogResultsResponse) def get_log_results(schedule_id: int, log_id: int, db: Session = Depends(get_db)): """取得某次排程執行的詳細逐項結果""" from app.models.result import TenantScheduleResult, AccountScheduleResult from app.models.tenant import Tenant from app.models.account import Account tenant_results = [] account_results = [] if schedule_id == 1: rows = ( db.query(TenantScheduleResult) .filter(TenantScheduleResult.schedule_log_id == log_id) .all() ) for r in rows: tenant = db.get(Tenant, r.tenant_id) tenant_results.append(TenantResultItem( tenant_id=r.tenant_id, tenant_name=tenant.name if tenant else None, traefik_status=r.traefik_status, sso_result=r.sso_result, mailbox_result=r.mailbox_result, nc_result=r.nc_result, office_result=r.office_result, quota_usage=r.quota_usage, fail_reason=r.fail_reason, )) elif schedule_id == 2: rows = ( db.query(AccountScheduleResult) .filter(AccountScheduleResult.schedule_log_id == log_id) .all() ) for r in rows: account_results.append(AccountResultItem( account_id=r.account_id, sso_account=r.sso_account, sso_result=r.sso_result, mailbox_result=r.mailbox_result, nc_result=r.nc_result, nc_mail_result=r.nc_mail_result, quota_usage=r.quota_usage, fail_reason=r.fail_reason, )) return LogResultsResponse( log_id=log_id, schedule_id=schedule_id, tenant_results=tenant_results, account_results=account_results, )