Files
autocoder/ui/src/hooks/useSchedules.ts
Marian Paul 0bab585630 feat: add time-based agent scheduling with APScheduler
Add comprehensive scheduling system that allows agents to automatically
start and stop during configured time windows, helping users manage
Claude API token limits by running agents during off-hours.

Backend Changes:
- Add Schedule and ScheduleOverride database models for persistent storage
- Implement APScheduler-based SchedulerService with UTC timezone support
- Add schedule CRUD API endpoints (/api/projects/{name}/schedules)
- Add manual override tracking to prevent unwanted auto-start/stop
- Integrate scheduler lifecycle with FastAPI startup/shutdown
- Fix timezone bug: explicitly set timezone=timezone.utc on CronTrigger
  to ensure correct UTC scheduling (critical fix)

Frontend Changes:
- Add ScheduleModal component for creating and managing schedules
- Add clock button and schedule status display to AgentControl
- Add timezone utilities for converting between UTC and local time
- Add React Query hooks for schedule data fetching
- Fix 204 No Content handling in fetchJSON for delete operations
- Invalidate nextRun cache when manually stopping agent during window
- Add TypeScript type annotations to Terminal component callbacks

Features:
- Multiple overlapping schedules per project supported
- Auto-start at scheduled time via APScheduler cron jobs
- Auto-stop after configured duration
- Manual start/stop creates persistent overrides in database
- Crash recovery with exponential backoff (max 3 retries)
- Server restart preserves schedules and active overrides
- Times displayed in user's local timezone, stored as UTC
- Immediate start if schedule created during active window

Dependencies:
- Add APScheduler for reliable cron-like scheduling

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 10:31:23 +01:00

113 lines
3.4 KiB
TypeScript

/**
* React Query hooks for schedule data
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import * as api from '../lib/api'
import type { ScheduleCreate, ScheduleUpdate } from '../lib/types'
// ============================================================================
// Schedules
// ============================================================================
/**
* Hook to fetch all schedules for a project.
*/
export function useSchedules(projectName: string | null) {
return useQuery({
queryKey: ['schedules', projectName],
queryFn: () => api.listSchedules(projectName!),
enabled: !!projectName,
})
}
/**
* Hook to fetch a single schedule.
*/
export function useSchedule(projectName: string | null, scheduleId: number | null) {
return useQuery({
queryKey: ['schedule', projectName, scheduleId],
queryFn: () => api.getSchedule(projectName!, scheduleId!),
enabled: !!projectName && !!scheduleId,
})
}
/**
* Hook to create a new schedule.
*/
export function useCreateSchedule(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (schedule: ScheduleCreate) => api.createSchedule(projectName, schedule),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['schedules', projectName] })
queryClient.invalidateQueries({ queryKey: ['nextRun', projectName] })
},
})
}
/**
* Hook to update an existing schedule.
*/
export function useUpdateSchedule(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ scheduleId, update }: { scheduleId: number; update: ScheduleUpdate }) =>
api.updateSchedule(projectName, scheduleId, update),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['schedules', projectName] })
queryClient.invalidateQueries({ queryKey: ['nextRun', projectName] })
},
})
}
/**
* Hook to delete a schedule.
*/
export function useDeleteSchedule(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (scheduleId: number) => api.deleteSchedule(projectName, scheduleId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['schedules', projectName] })
queryClient.invalidateQueries({ queryKey: ['nextRun', projectName] })
},
})
}
/**
* Hook to toggle a schedule's enabled state.
*/
export function useToggleSchedule(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ scheduleId, enabled }: { scheduleId: number; enabled: boolean }) =>
api.updateSchedule(projectName, scheduleId, { enabled }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['schedules', projectName] })
queryClient.invalidateQueries({ queryKey: ['nextRun', projectName] })
},
})
}
// ============================================================================
// Next Run
// ============================================================================
/**
* Hook to fetch the next scheduled run for a project.
* Polls every 30 seconds to keep status up-to-date.
*/
export function useNextScheduledRun(projectName: string | null) {
return useQuery({
queryKey: ['nextRun', projectName],
queryFn: () => api.getNextScheduledRun(projectName!),
enabled: !!projectName,
refetchInterval: 30000, // Refresh every 30 seconds
})
}