import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'; import { Loader2 } from 'lucide-react'; import { apiFetch } from '@/lib/api'; import type { ScheduledTask } from '@/types/scheduling'; import { Masthead, SectionHead, StateDot } from './mobile-ui'; interface MobileSchedulesProps { headerActions: ReactNode; } type Tone = 'success' | 'warning' | 'destructive' | 'brand'; const ACTION_TONE: Record = { restart: 'brand', update: 'success', scan: 'success', prune: 'warning', snapshot: 'warning', auto_backup: 'brand', auto_stop: 'warning', auto_down: 'destructive', auto_start: 'success', }; const ACTION_LABEL: Record = { restart: 'restart', update: 'update', scan: 'scan', prune: 'prune', snapshot: 'snapshot', auto_backup: 'backup', auto_stop: 'stop', auto_down: 'down', auto_start: 'start', }; function hhmm(ts: number): string { const d = new Date(ts); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; } function relative(ts: number, now: number): string { const diff = ts - now; if (diff <= 0) return 'now'; const mins = Math.round(diff / 60_000); if (mins < 60) return `in ${mins}m`; const hours = Math.floor(mins / 60); const rem = mins % 60; return rem === 0 ? `in ${hours}h` : `in ${hours}h ${rem}m`; } function dayLabel(ts: number, now: number): string { const d = new Date(ts); const n = new Date(now); const startOf = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); const dayDiff = Math.round((startOf(d) - startOf(n)) / 86_400_000); if (dayDiff <= 0) return 'Today'; if (dayDiff === 1) return 'Tomorrow'; return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); } function targetLabel(task: ScheduledTask): string { if (task.target_type === 'stack') return (task.target_id ?? task.name).replace(/\.(ya?ml)$/, ''); if (task.target_type === 'fleet') return 'fleet'; return task.target_type; } interface UpcomingRun { task: ScheduledTask; runAt: number; } export function MobileSchedules({ headerActions }: MobileSchedulesProps) { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [now, setNow] = useState(() => Date.now()); const abortRef = useRef(null); const fetchTasks = useCallback(async () => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; try { const res = await apiFetch('/scheduled-tasks', { localOnly: true, signal: controller.signal }); if (res.ok) { setTasks(await res.json() as ScheduledTask[]); } else { console.error('Scheduled tasks poll failed:', res.status); } } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') return; console.error('Failed to fetch scheduled tasks:', error); } finally { setLoading(false); } }, []); useEffect(() => { // fetchTasks sets state only after an await, so it does not cause the // synchronous cascading render this rule guards against; the rule flags the // call conservatively because it can't follow the async boundary. // eslint-disable-next-line react-hooks/set-state-in-effect void fetchTasks(); const id = setInterval(() => void fetchTasks(), 60_000); return () => { clearInterval(id); abortRef.current?.abort(); }; }, [fetchTasks]); useEffect(() => { const id = setInterval(() => setNow(Date.now()), 30_000); return () => clearInterval(id); }, []); const enabledCount = tasks.filter(t => t.enabled === 1).length; const upcoming: UpcomingRun[] = tasks .filter(t => t.enabled === 1 && t.next_runs && t.next_runs.length > 0) .flatMap(task => (task.next_runs ?? []).map(runAt => ({ task, runAt }))) .filter(p => p.runAt >= now) .sort((a, b) => a.runAt - b.runAt) .slice(0, 60); const next = upcoming[0] ?? null; return (
{loading && tasks.length === 0 ? (
) : upcoming.length === 0 ? (

Nothing scheduled. Create a schedule on desktop to automate recurring operations.

) : ( upcoming.map((run, i) => { const prevDay = i > 0 ? dayLabel(upcoming[i - 1].runAt, now) : null; const day = dayLabel(run.runAt, now); const tone = ACTION_TONE[run.task.action]; return (
{day !== prevDay ? {day} : null}
{hhmm(run.runAt)} {ACTION_LABEL[run.task.action]}{` ${targetLabel(run.task)}`} {relative(run.runAt, now)}
); }) )}
); }