import { useCallback, useEffect, useRef, useState } from 'react' import { api, CronJob, CronOverview, CronRun, CronTimer, LogEntry } from './api' import { Logs } from './Services' import { Btn, Loading, Spinner, toast } from './ui' const fmtTime = (ms: number) => { const d = new Date(ms); return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: '2-digit' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) } function rel(ms: number): string { const s = Math.round((ms - Date.now()) / 1000), a = Math.abs(s) const d = Math.floor(a / 86400), h = Math.floor((a % 86400) / 3600), m = Math.floor((a % 3600) / 60) const txt = d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m` : `${a}s` return s >= 0 ? `in ${txt}` : `${txt} ago` } // Plain-English reading of the common five-field shapes; anything else shows the raw expression. function describe(expr: string): string { const alias: Record = { '@reboot': 'at boot', '@hourly': 'every hour', '@daily': 'every day at 00:00', '@midnight': 'every day at 00:00', '@weekly': 'every Sunday at 00:00', '@monthly': 'on the 1st at 00:00', '@yearly': 'every 1 Jan', '@annually': 'every 1 Jan' } if (alias[expr]) return alias[expr] const f = expr.split(/\s+/) if (f.length !== 5) return expr const [mi, h, dom, mon, dow] = f const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] const at = /^\d+$/.test(mi) && /^\d+$/.test(h) ? `at ${h.padStart(2, '0')}:${mi.padStart(2, '0')}` : /^\d+$/.test(mi) && h === '*' ? `every hour at :${mi.padStart(2, '0')}` : mi.startsWith('*/') && h === '*' ? `every ${mi.slice(2)} min` : null if (!at) return expr const dowTxt = dow === '*' ? '' : /^\d$/.test(dow) ? `every ${days[+dow]}` : `on days ${dow}` const domTxt = dom === '*' ? '' : /^\d+$/.test(dom) ? `on the ${dom}${['st', 'nd', 'rd'][+dom - 1] ?? 'th'}` : `on days ${dom}` const monTxt = mon === '*' ? '' : `in month ${mon}` const when = [dowTxt, domTxt, monTxt].filter(Boolean).join(', ') return (when ? `${when} ` : dow === '*' && dom === '*' && !at.startsWith('every') ? 'every day ' : '') + at } const srcLabel = (s: string) => (s.startsWith('crontab:') ? `${s.slice(8)}'s crontab` : s) // ---- source editor ---- function SourceEditor({ name, onClose, onSaved }: { name: string; onClose: () => void; onSaved: () => void }) { const [text, setText] = useState(null) const [editable, setEditable] = useState(false) const [err, setErr] = useState('') const [busy, setBusy] = useState(false) useEffect(() => { api.cronSource(name).then((r) => { setText(r.text); setEditable(r.editable) }).catch((e) => setErr(String(e))) }, [name]) async function save() { if (text === null) return setBusy(true); setErr('') try { await api.cronWriteSource(name, text); toast(`${srcLabel(name)} saved`); onSaved(); onClose() } catch (e) { setErr(e instanceof Error ? e.message : 'save failed') } finally { setBusy(false) } } return (

{srcLabel(name)}

{!editable &&
Read-only: writing this file needs passwordless sudo on the host.
} {text === null ? :