/* =====================================================================
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 =
'' +
'| Name | Command | Schedule | Workers | Last run | Next run | Status | Actions |
' +
'' + _schedules.map(rowHtml).join('') + '' +
'
';
}
function renderError() {
const wrap = document.getElementById('sc-wrap');
if (!wrap) return;
wrap.innerHTML =
'';
}
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 = '';
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 });
})();