Spaces:
Sleeping
Sleeping
| export function describeCronFrequency(schedule: string): string { | |
| const parts = schedule.replace(/\s*\([^)]+\)$/, '').trim().split(/\s+/) | |
| if (parts.length !== 5) return schedule | |
| const [minute, hour, dom, mon, dow] = parts | |
| // Every minute | |
| if (minute === '*' && hour === '*') return 'every minute' | |
| // Every N minutes | |
| if (minute.startsWith('*/') && hour === '*') return `every ${minute.slice(2)}m` | |
| // Every hour at :MM | |
| if (/^\d+$/.test(minute) && hour === '*') return `hourly at :${minute.padStart(2, '0')}` | |
| // Every N hours | |
| if (/^\d+$/.test(minute) && hour.startsWith('*/')) return `every ${hour.slice(2)}h` | |
| // Specific hour(s) daily | |
| if (/^\d+$/.test(minute) && /^\d+$/.test(hour) && dom === '*' && mon === '*') { | |
| const h = Number(hour) | |
| const m = Number(minute) | |
| const time = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}` | |
| if (dow !== '*') return `${time} (select days)` | |
| return `daily at ${time}` | |
| } | |
| // Weekly | |
| if (dom === '*' && mon === '*' && dow !== '*') return 'weekly' | |
| // Monthly | |
| if (dom !== '*' && mon === '*' && dow === '*') return 'monthly' | |
| return schedule | |
| } | |
| export function validateCronExpression(expr: string): string | null { | |
| if (!expr || !expr.trim()) return 'Cron expression is required' | |
| const parts = expr.trim().split(/\s+/) | |
| if (parts.length !== 5) return `Expected 5 fields, got ${parts.length}` | |
| const fieldNames = ['minute', 'hour', 'day of month', 'month', 'day of week'] | |
| const maxValues = [59, 23, 31, 12, 7] | |
| const minValues = [0, 0, 1, 1, 0] | |
| for (let i = 0; i < 5; i++) { | |
| const field = parts[i] | |
| if (field === '*') continue | |
| // Step values: */N | |
| if (field.startsWith('*/')) { | |
| const step = Number(field.slice(2)) | |
| if (isNaN(step) || step < 1) return `Invalid step value in ${fieldNames[i]}: ${field}` | |
| continue | |
| } | |
| // Comma-separated values and ranges | |
| const segments = field.split(',') | |
| for (const segment of segments) { | |
| // Range: N-M | |
| const rangeParts = segment.split('-') | |
| for (const part of rangeParts) { | |
| const num = Number(part) | |
| if (isNaN(num)) return `Invalid value in ${fieldNames[i]}: ${part}` | |
| if (num < minValues[i] || num > maxValues[i]) { | |
| return `${fieldNames[i]} value ${num} out of range (${minValues[i]}-${maxValues[i]})` | |
| } | |
| } | |
| } | |
| } | |
| return null | |
| } | |
| export function generateCloneName(name: string, existingNames: string[]): string { | |
| const existing = new Set(existingNames.map(n => n.toLowerCase())) | |
| let cloneName = `${name} (copy)` | |
| let counter = 2 | |
| while (existing.has(cloneName.toLowerCase())) { | |
| cloneName = `${name} (copy ${counter})` | |
| counter++ | |
| } | |
| return cloneName | |
| } | |