// ─── Cron Page ─────────────────────────────────────────────────────────────── const { useState: uCRS, useEffect: uCRE } = React; function parseCron(schedule) { const presets = { '0 2 * * *': 'Daily at 02:00', '0 3 * * 1': 'Every Monday at 03:00', '0 1 1 * *': '1st of month at 01:00', '*/30 * * * *': 'Every 30 minutes', '0 8 * * *': 'Daily at 08:00', '0 4 * * 0': 'Every Sunday at 04:00', }; return presets[schedule] || schedule; } function CronPage() { const [jobs, setJobs] = uCRS([]); const [scripts, setScripts] = uCRS([]); const [addModal, setAddModal] = uCRS(false); const [rawModal, setRawModal] = uCRS(false); const [rawText, setRawText] = uCRS(''); const [newJob, setNewJob] = uCRS({ name: '', script: '', schedule: '0 2 * * *', minute: '0', hour: '2', dom: '*', month: '*', dow: '*', ntfy: false }); const [toast, setToast] = uCRS(null); const showToast = (msg, color = '#00ff88') => { setToast({ msg, color }); setTimeout(() => setToast(null), 2500); }; const fetchJobs = () => apiClient('/api/cron/jobs').then(d => { if (d) setJobs(d); }).catch(() => {}); uCRE(() => { fetchJobs(); apiClient('/api/scripts').then(d => { if (d) setScripts(d.map(s => s.name)); }).catch(() => {}); const id = setInterval(fetchJobs, 30000); return () => clearInterval(id); }, []); const toggleActive = async (id) => { const j = jobs.find(x => x.id === id); if (!j) return; try { await apiClient(`/api/cron/jobs/${id}`, { method: 'PUT', body: JSON.stringify({ active: !j.active }), }); fetchJobs(); } catch (err) { showToast(`Failed: ${err.message}`, '#ff2244'); } }; const toggleNtfy = async (id) => { const j = jobs.find(x => x.id === id); if (!j) return; try { await apiClient(`/api/cron/jobs/${id}`, { method: 'PUT', body: JSON.stringify({ ntfy: !j.ntfy }), }); fetchJobs(); showToast('NTFY setting updated'); } catch (err) { showToast(`Failed: ${err.message}`, '#ff2244'); } }; const deleteJob = async (id) => { const j = jobs.find(x => x.id === id); try { await apiClient(`/api/cron/jobs/${id}`, { method: 'DELETE' }); showToast(`Deleted: ${j?.name}`, '#ff2244'); fetchJobs(); } catch (err) { showToast(`Failed: ${err.message}`, '#ff2244'); } }; const addJob = async () => { const schedule = `${newJob.minute} ${newJob.hour} ${newJob.dom} ${newJob.month} ${newJob.dow}`; try { await apiClient('/api/cron/jobs', { method: 'POST', body: JSON.stringify({ name: newJob.name || newJob.script, schedule, script: newJob.script, ntfy: newJob.ntfy }), }); setAddModal(false); showToast(`Job "${newJob.name || newJob.script}" added`); fetchJobs(); } catch (err) { showToast(`Failed: ${err.message}`, '#ff2244'); } }; const openRaw = async () => { const data = await apiClient('/api/cron/raw').catch(() => null); setRawText(data?.crontab_text || ''); setRawModal(true); }; const statusColor = { success: '#00ff88', error: '#ff2244' }; const scriptsList = scripts.length > 0 ? scripts : ['daily-backup.sh', 'update-system.sh', 'health-check.py']; return (
{/* Job Table */} RAW CRONTAB setAddModal(true)}>+ ADD JOB
}>
{['NAME', 'SCHEDULE', 'SCRIPT', 'LAST RUN', 'NEXT RUN', 'STATUS', 'ACTIVE', 'NTFY', ''].map(h => ( ))} {jobs.length === 0 && ( )} {jobs.map(j => ( ))}
{h}
NO JOBS CONFIGURED
{j.name}
{j.schedule}
{parseCron(j.schedule)}
{j.script} {j.last_run || '—'} {j.next_run ? j.next_run.replace('T',' ').slice(0,16) : '—'} {j.last_status && ● {j.last_status.toUpperCase()}} toggleActive(j.id)} /> toggleNtfy(j.id)} /> deleteJob(j.id)}>DEL
{/* Stats Panel */}
{[ ['TOTAL', jobs.length, '#00c8ff'], ['ACTIVE', jobs.filter(j=>j.active).length, '#00ff88'], ['NTFY ON', jobs.filter(j=>j.ntfy).length, '#ffd700'], ['ERRORS', jobs.filter(j=>j.last_status==='error').length, '#ff2244'], ].map(([l, v, c]) => (
{v}
{l}
))}
{jobs.filter(j => j.active && j.next_run).sort((a,b) => a.next_run.localeCompare(b.next_run)).map(j => (
{j.name}
{j.next_run.replace('T',' ').slice(0,16)}
))}
{/* Add Job Modal */} setAddModal(false)} title="ADD CRON JOB" width={560}> setNewJob(p => ({...p, name: v}))} placeholder="e.g. Weekly Report" />
SCHEDULE (CRON EXPRESSION)
{[['MINUTE', 'minute', '0-59'], ['HOUR', 'hour', '0-23'], ['DOM', 'dom', '1-31'], ['MONTH', 'month', '1-12'], ['DOW', 'dow', '0-6']].map(([l, k, hint]) => (
{l}
setNewJob(p => ({...p, [k]: e.target.value, schedule: `${k === 'minute' ? e.target.value : p.minute} ${k === 'hour' ? e.target.value : p.hour} ${k === 'dom' ? e.target.value : p.dom} ${k === 'month' ? e.target.value : p.month} ${k === 'dow' ? e.target.value : p.dow}` }))} placeholder={hint} style={{ textAlign: 'center' }} />
))}
{newJob.schedule} — /opt/scripts/{newJob.script}
setNewJob(p => ({...p, ntfy: v}))} label="Send NTFY on completion" />
setAddModal(false)}>CANCEL ADD JOB
{/* Raw Crontab Modal */} setRawModal(false)} title="RAW CRONTAB VIEW" width={680}> {}} rows={16} />
setRawModal(false)}>CLOSE
{toast && (
{toast.msg}
)} ); } Object.assign(window, { CronPage });