/** * Cron utilities * Parse schedules and estimate next run times */ import type { CronSchedule } from '@lib/cron-types'; export type ScheduleType = 'daily' | 'weekly' | 'monthly' | 'interval' | 'custom'; export interface SchedulePreset { key: string; value: string; type: ScheduleType; } export const schedulePresets: SchedulePreset[] = [ { key: 'everyMinute', value: '* * * * *', type: 'interval' }, { key: 'every5Min', value: '*/5 * * * *', type: 'interval' }, { key: 'every15Min', value: '*/15 * * * *', type: 'interval' }, { key: 'everyHour', value: '0 * * * *', type: 'interval' }, { key: 'daily9am', value: '0 9 * * *', type: 'daily' }, { key: 'daily6pm', value: '0 18 * * *', type: 'daily' }, { key: 'weeklyMon', value: '0 9 * * 1', type: 'weekly' }, { key: 'monthly1st', value: '0 9 1 * *', type: 'monthly' }, ]; // Parse cron schedule to human-readable format // Handles both plain cron strings and Gateway CronSchedule objects export function parseCronSchedule( schedule: unknown, t: (key: string, interpolations?: Record) => string, ): string { if (schedule && typeof schedule === 'object') { const s = schedule as { kind?: string; expr?: string; tz?: string; everyMs?: number; at?: string }; if (s.kind === 'cron' && typeof s.expr === 'string') { return parseCronExpr(s.expr, t); } if (s.kind === 'every' && typeof s.everyMs === 'number') { const ms = s.everyMs; if (ms < 60_000) return t('cron.schedule.everySeconds', { count: Math.round(ms / 1000) }); if (ms < 3_600_000) return t('cron.schedule.everyMinutes', { count: Math.round(ms / 60_000) }); if (ms < 86_400_000) return t('cron.schedule.everyHours', { count: Math.round(ms / 3_600_000) }); return t('cron.schedule.everyDays', { count: Math.round(ms / 86_400_000) }); } if (s.kind === 'at' && typeof s.at === 'string') { try { return t('cron.schedule.onceAt', { time: new Date(s.at).toLocaleString() }); } catch { return t('cron.schedule.onceAt', { time: s.at }); } } return String(schedule); } if (typeof schedule === 'string') { return parseCronExpr(schedule, t); } return String(schedule ?? t('cron.schedule.unknown')); } export function parseCronExpr( cron: string, t: (key: string, interpolations?: Record) => string, ): string { const preset = schedulePresets.find((p) => p.value === cron); if (preset) return t(`cron.presets.${preset.key}` as const); const parts = cron.split(' '); if (parts.length !== 5) return cron; const [minute, hour, dayOfMonth, , dayOfWeek] = parts; if (minute === '*' && hour === '*') return t('cron.presets.everyMinute'); if (minute.startsWith('*/')) return t('cron.schedule.everyMinutes', { count: Number(minute.slice(2)) }); if (hour === '*' && minute === '0') return t('cron.presets.everyHour'); if (dayOfWeek !== '*' && dayOfMonth === '*') { return t('cron.schedule.weeklyAt', { day: dayOfWeek, time: `${hour}:${minute.padStart(2, '0')}` }); } if (dayOfMonth !== '*') { return t('cron.schedule.monthlyAtDay', { day: dayOfMonth, time: `${hour}:${minute.padStart(2, '0')}` }); } if (hour !== '*') { return t('cron.schedule.dailyAt', { time: `${hour}:${minute.padStart(2, '0')}` }); } return cron; } export function estimateNextRun(scheduleExpr: string): string | null { const now = new Date(); const next = new Date(now.getTime()); if (scheduleExpr === '* * * * *') { next.setSeconds(0, 0); next.setMinutes(next.getMinutes() + 1); return formatLocaleDateTime(next); } if (scheduleExpr === '*/5 * * * *') { const delta = 5 - (next.getMinutes() % 5 || 5); next.setSeconds(0, 0); next.setMinutes(next.getMinutes() + delta); return formatLocaleDateTime(next); } if (scheduleExpr === '*/15 * * * *') { const delta = 15 - (next.getMinutes() % 15 || 15); next.setSeconds(0, 0); next.setMinutes(next.getMinutes() + delta); return formatLocaleDateTime(next); } if (scheduleExpr === '0 * * * *') { next.setMinutes(0, 0, 0); next.setHours(next.getHours() + 1); return formatLocaleDateTime(next); } if (scheduleExpr === '0 9 * * *' || scheduleExpr === '0 18 * * *') { const targetHour = scheduleExpr === '0 9 * * *' ? 9 : 18; next.setSeconds(0, 0); next.setHours(targetHour, 0, 0, 0); if (next <= now) next.setDate(next.getDate() + 1); return formatLocaleDateTime(next); } if (scheduleExpr === '0 9 * * 1') { next.setSeconds(0, 0); next.setHours(9, 0, 0, 0); const day = next.getDay(); const daysUntilMonday = day === 1 ? 7 : (8 - day) % 7; next.setDate(next.getDate() + daysUntilMonday); return formatLocaleDateTime(next); } if (scheduleExpr === '0 9 1 * *') { next.setSeconds(0, 0); next.setDate(1); next.setHours(9, 0, 0, 0); if (next <= now) next.setMonth(next.getMonth() + 1); return formatLocaleDateTime(next); } return null; } function formatLocaleDateTime(date: Date): string { try { return new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }).format(date); } catch { return date.toLocaleString(); } }