/* ===================================================================== PULSE — Scheduler page (WP-F) Owns DOM id prefix `sc-` and action namespace `sc:*`. ===================================================================== */ 'use strict'; (function () { const esc = Pulse.esc; const fmt = Pulse.fmt; let _schedules = []; let _workers = []; /* ------------------------------------------------------------------- Helpers ------------------------------------------------------------------- */ function parseWorkerIds(raw) { if (Array.isArray(raw)) return raw; if (typeof raw === 'string') { try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : raw.split(',').filter(Boolean); } catch (e) { return raw.split(',').filter(Boolean); } } return []; } function scheduleDescription(s) { if (s.schedule_type === 'interval') return 'Every ' + esc(s.schedule_value) + ' minutes'; if (s.schedule_type === 'hourly') return 'Every ' + esc(s.schedule_value) + ' hour(s)'; if (s.schedule_type === 'daily') return 'Daily at ' + esc(s.schedule_value); if (s.schedule_type === 'cron') return 'Cron: ' + esc(s.schedule_value); return esc(s.schedule_value || ''); } function workerNamesHtml(ids) { if (!ids.length) return '—'; return ids.map(id => { const w = _workers.find(worker => worker.id === id); const label = w ? w.name : String(id).slice(0, 8); return '' + esc(label) + ''; }).join(' '); } function nextRunCountdown(nextRun) { const d = fmt.safeDate(nextRun); if (!d) return ''; const secs = Math.round((d.getTime() - Date.now()) / 1000); if (secs <= 0) return 'now'; if (secs < 60) return secs + 's'; if (secs < 3600) return Math.round(secs / 60) + 'm'; return Math.round(secs / 3600) + 'h'; } /* ------------------------------------------------------------------- Table rendering ------------------------------------------------------------------- */ function rowHtml(s) { const workerIds = parseWorkerIds(s.worker_ids); const lastRun = s.last_run ? esc(fmt.dateTime(s.last_run)) : 'Never'; const nextRunDate = fmt.safeDate(s.next_run); const countdown = nextRunCountdown(s.next_run); const nextRun = nextRunDate ? esc(fmt.dateTime(s.next_run)) + (countdown ? ' (in ' + esc(countdown) + ')' : '') : 'Not scheduled'; const statusBadge = s.enabled ? 'ENABLED' : 'DISABLED'; const adminActions = Pulse.isAdmin ? '
' + '' + '' + '
' : ''; return ( '' + '' + esc(s.name || '') + '' + '' + esc(s.command || '') + '' + '' + scheduleDescription(s) + '' + '' + workerNamesHtml(workerIds) + '' + '' + lastRun + '' + '' + nextRun + '' + '' + statusBadge + '' + '' + adminActions + '' + '' ); } function render() { const wrap = document.getElementById('sc-wrap'); if (!wrap) return; if (_schedules.length === 0) { wrap.innerHTML = '
No scheduled commands yet
'; return; } wrap.innerHTML = '' + '' + '' + _schedules.map(rowHtml).join('') + '' + '
NameCommandScheduleWorkersLast runNext runStatusActions
'; } function renderError() { const wrap = document.getElementById('sc-wrap'); if (!wrap) return; wrap.innerHTML = '
' + '✕' + '
Failed to load schedules
' + '
'; } function onTick() { document.querySelectorAll('#sc-wrap .sc-countdown[data-next-run]').forEach(el => { const countdown = nextRunCountdown(el.getAttribute('data-next-run')); el.textContent = countdown ? '(in ' + countdown + ')' : ''; }); } /* ------------------------------------------------------------------- Create modal ------------------------------------------------------------------- */ function renderCreateWorkerList() { const wrap = document.getElementById('sc-worker-list'); if (!wrap) return; if (_workers.length === 0) { wrap.innerHTML = '
No workers available
'; return; } wrap.innerHTML = _workers.map(w => '' ).join(''); } function updateValueInput() { const type = document.getElementById('sc-type').value; const container = document.getElementById('sc-value-container'); if (!container) return; if (type === 'interval') { container.innerHTML = '' + ''; } else if (type === 'hourly') { container.innerHTML = '' + ''; } else if (type === 'daily') { container.innerHTML = '' + ''; } } function resetCreateForm() { const form = document.getElementById('sc-create-form'); if (form) form.reset(); const type = document.getElementById('sc-type'); if (type) type.value = 'interval'; updateValueInput(); renderCreateWorkerList(); } async function submitCreate() { const name = document.getElementById('sc-name').value.trim(); const command = document.getElementById('sc-command').value.trim(); const type = document.getElementById('sc-type').value; const valueEl = document.getElementById('sc-value'); const value = valueEl ? valueEl.value : ''; const workerIds = Array.from(document.querySelectorAll('input[name="sc-worker-cb"]:checked')).map(cb => cb.value); if (!name || !command || !value || workerIds.length === 0) { Pulse.toast.error('Please fill in all fields and select at least one worker'); return; } try { await Pulse.api.post('/api/scheduled-commands', { name: name, command: command, worker_ids: workerIds, schedule_type: type, schedule_value: value, }); if (window.lt && lt.modal) lt.modal.close('sc-create-modal'); Pulse.toast.success('Schedule created successfully'); resetCreateForm(); await load(); } catch (e) { Pulse.toast.error('Failed to create schedule: ' + (e.message || 'unknown error')); } } async function toggleSchedule(id) { try { const data = await Pulse.api.put('/api/scheduled-commands/' + encodeURIComponent(id) + '/toggle'); Pulse.toast.success('Schedule ' + (data && data.enabled ? 'enabled' : 'disabled')); await load(); } catch (e) { Pulse.toast.error('Failed to toggle schedule: ' + (e.message || 'unknown error')); } } async function deleteSchedule(id, name) { const ok = await Pulse.confirm({ title: 'Delete schedule', message: 'Delete scheduled command: ' + name + '?', type: 'error', confirmLabel: 'DELETE', }); if (!ok) return; try { await Pulse.api.delete('/api/scheduled-commands/' + encodeURIComponent(id)); Pulse.toast.success('Schedule deleted'); await load(); } catch (e) { Pulse.toast.error('Failed to delete schedule: ' + (e.message || 'unknown error')); } } /* ------------------------------------------------------------------- Data loading ------------------------------------------------------------------- */ async function loadWorkers() { try { _workers = await Pulse.api.get('/api/workers') || []; } catch (e) { _workers = []; console.error('[Pulse:scheduler] failed to load workers', e); } return _workers; } async function loadSchedules() { try { _schedules = await Pulse.api.get('/api/scheduled-commands') || []; render(); } catch (e) { _schedules = []; console.error('[Pulse:scheduler] failed to load schedules', e); renderError(); } return _schedules; } async function load() { await loadWorkers(); await loadSchedules(); } async function refresh() { await load(); } function init() { Pulse.actions.registerAll({ 'sc:create-open': () => { resetCreateForm(); if (window.lt && lt.modal) lt.modal.open('sc-create-modal'); }, 'sc:create-submit': submitCreate, 'sc:type': updateValueInput, 'sc:toggle': (el) => toggleSchedule(el.getAttribute('data-id')), 'sc:delete': (el) => deleteSchedule(el.getAttribute('data-id'), el.getAttribute('data-name')), }); Pulse.events.on('tick', onTick); return load(); } function onEvent(type) { if (type === 'worker_update') { loadWorkers().then(render); return true; } return false; } Pulse.registerPage({ name: 'scheduler', init, refresh, onEvent }); })();