/** * Schedule Modal Component * * Modal for managing agent schedules (create, edit, delete). * Follows neobrutalism design patterns from SettingsModal. */ import { useState, useEffect, useRef } from 'react' import { Clock, Trash2, X } from 'lucide-react' import { useSchedules, useCreateSchedule, useDeleteSchedule, useToggleSchedule, } from '../hooks/useSchedules' import { utcToLocal, localToUTC, formatDuration, DAYS, isDayActive, toggleDay, } from '../lib/timeUtils' import type { ScheduleCreate } from '../lib/types' interface ScheduleModalProps { projectName: string isOpen: boolean onClose: () => void } export function ScheduleModal({ projectName, isOpen, onClose }: ScheduleModalProps) { const modalRef = useRef(null) const firstFocusableRef = useRef(null) // Queries and mutations const { data: schedulesData, isLoading } = useSchedules(projectName) const createSchedule = useCreateSchedule(projectName) const deleteSchedule = useDeleteSchedule(projectName) const toggleSchedule = useToggleSchedule(projectName) // Form state for new schedule const [newSchedule, setNewSchedule] = useState({ start_time: '22:00', duration_minutes: 240, days_of_week: 31, // Weekdays by default enabled: true, yolo_mode: false, model: null, }) const [error, setError] = useState(null) // Focus trap useEffect(() => { if (isOpen && firstFocusableRef.current) { firstFocusableRef.current.focus() } }, [isOpen]) // Keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!isOpen) return if (e.key === 'Escape') { onClose() } if (e.key === 'Tab' && modalRef.current) { const focusableElements = modalRef.current.querySelectorAll( 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])' ) const firstElement = focusableElements[0] const lastElement = focusableElements[focusableElements.length - 1] if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault() lastElement?.focus() } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault() firstElement?.focus() } } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [isOpen, onClose]) if (!isOpen) return null const schedules = schedulesData?.schedules || [] const handleCreateSchedule = async () => { try { setError(null) // Validate if (newSchedule.days_of_week === 0) { setError('Please select at least one day') return } // Convert local time to UTC const scheduleToCreate = { ...newSchedule, start_time: localToUTC(newSchedule.start_time), } await createSchedule.mutateAsync(scheduleToCreate) // Reset form setNewSchedule({ start_time: '22:00', duration_minutes: 240, days_of_week: 31, enabled: true, yolo_mode: false, model: null, }) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create schedule') } } const handleToggleSchedule = async (scheduleId: number, enabled: boolean) => { try { setError(null) await toggleSchedule.mutateAsync({ scheduleId, enabled: !enabled }) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to toggle schedule') } } const handleDeleteSchedule = async (scheduleId: number) => { if (!confirm('Are you sure you want to delete this schedule?')) return try { setError(null) await deleteSchedule.mutateAsync(scheduleId) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete schedule') } } const handleToggleDay = (dayBit: number) => { setNewSchedule((prev) => ({ ...prev, days_of_week: toggleDay(prev.days_of_week, dayBit), })) } return (
{ if (e.target === e.currentTarget) { onClose() } }} >
{/* Header */}

Agent Schedules

{/* Error display */} {error && (
{error}
)} {/* Loading state */} {isLoading && (
Loading schedules...
)} {/* Existing schedules */} {!isLoading && schedules.length > 0 && (
{schedules.map((schedule) => { const localTime = utcToLocal(schedule.start_time) const duration = formatDuration(schedule.duration_minutes) return (
{/* Time and duration */}
{localTime} for {duration}
{/* Days */}
{DAYS.map((day) => { const isActive = isDayActive(schedule.days_of_week, day.bit) return ( {day.label} ) })}
{/* Metadata */}
{schedule.yolo_mode && ( ⚡ YOLO mode )} {schedule.model && Model: {schedule.model}} {schedule.crash_count > 0 && ( Crashes: {schedule.crash_count} )}
{/* Actions */}
{/* Enable/disable toggle */} {/* Delete button */}
) })}
)} {/* Empty state */} {!isLoading && schedules.length === 0 && (

No schedules configured yet

)} {/* Divider */}
{/* Add new schedule form */}

Add New Schedule

{/* Time and duration */}
setNewSchedule((prev) => ({ ...prev, start_time: e.target.value })) } className="neo-input w-full" />
setNewSchedule((prev) => ({ ...prev, duration_minutes: parseInt(e.target.value) || 0, })) } className="neo-input w-full" />
{formatDuration(newSchedule.duration_minutes)}
{/* Days of week */}
{DAYS.map((day) => { const isActive = isDayActive(newSchedule.days_of_week, day.bit) return ( ) })}
{/* YOLO mode toggle */}
{/* Model selection (optional) */}
setNewSchedule((prev) => ({ ...prev, model: e.target.value || null })) } className="neo-input w-full" />
{/* Actions */}
) }