mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-31 14:43:35 +00:00
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>
113 lines
3.4 KiB
TypeScript
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
|
|
})
|
|
}
|