/* =====================================================================
PULSE — Dashboard page (WP-C)
Owns DOM id prefix `dash-` and action namespace `dash:*`.
===================================================================== */
'use strict';
(function () {
const esc = Pulse.esc;
const fmt = Pulse.fmt;
let _executions = [];
let _workers = [];
function parseMeta(worker) {
if (!worker || !worker.metadata) return null;
if (typeof worker.metadata === 'string') {
try { return JSON.parse(worker.metadata); } catch (e) { return null; }
}
return worker.metadata;
}
function isAutomated(exec) {
const by = String((exec && exec.started_by) || '');
return by.indexOf('gandalf:') === 0 || by.indexOf('scheduler:') === 0;
}
/* -------------------------------------------------------------------
Stats
------------------------------------------------------------------- */
function renderStats(workers, runningTotal) {
const total = workers.length;
const online = workers.filter(w => w.status === 'online').length;
const offline = total - online;
const set = (id, val) => {
const el = document.getElementById(id);
if (el) el.textContent = String(val);
};
set('dash-stat-total-val', total);
set('dash-stat-online-val', online);
set('dash-stat-offline-val', offline);
set('dash-stat-running-val', runningTotal);
}
/* -------------------------------------------------------------------
Recent executions (5 most recent manual runs)
------------------------------------------------------------------- */
function executionRowHtml(e) {
const statusClass = fmt.status(e.status);
const statusText = esc(String(e.status || '').toUpperCase());
const name = e.workflow_name ? esc(e.workflow_name) : '[Quick Command]';
const startedBy = esc(e.started_by || '');
const startedAt = fmt.dateTime(e.started_at);
const isRunning = String(e.status || '').toLowerCase() === 'running';
const elapsed = isRunning ? esc(fmt.elapsed(e.started_at)) : '—';
return (
'
' +
'| ' + statusText + ' | ' +
'' + name + ' | ' +
'' + startedBy + ' | ' +
'' + esc(startedAt) + ' | ' +
'' + elapsed + ' | ' +
'
'
);
}
function renderExecutions(executions) {
const wrap = document.getElementById('dash-executions-wrap');
if (!wrap) return;
const manual = executions.filter(e => !isAutomated(e)).slice(0, 5);
if (manual.length === 0) {
wrap.innerHTML =
'' +
'
No executions yet
' +
'
';
return;
}
wrap.innerHTML =
'' +
'| Status | Name | Started By | Started At | Elapsed |
' +
'' + manual.map(executionRowHtml).join('') + '' +
'
';
}
/* -------------------------------------------------------------------
Workers summary
------------------------------------------------------------------- */
function workerRowHtml(w) {
const meta = parseMeta(w);
const dotClass = w.status === 'online' ? 'lt-dot-up' : 'lt-dot-down';
const statusClass = fmt.status(w.status);
const lastSeen = fmt.ago(w.last_heartbeat);
let stats = '';
if (meta) {
stats =
'CPU: ' + esc(meta.cpus || '?') + ' cores' +
'RAM: ' + fmt.bytes(meta.totalMem - meta.freeMem) + ' / ' + fmt.bytes(meta.totalMem) + '' +
'Tasks: ' + esc(meta.activeTasks || 0) + '/' + esc(meta.maxConcurrentTasks || 0) + '';
}
return (
'' +
'
' +
'' +
'' + esc(w.name) + '' +
'' + esc(String(w.status || '').toUpperCase()) + '' +
'Last seen: ' + esc(lastSeen) + '' +
'
' +
(stats ? '
' + stats + '
' : '') +
'
'
);
}
function renderWorkers(workers) {
const wrap = document.getElementById('dash-workers-wrap');
if (!wrap) return;
if (workers.length === 0) {
wrap.innerHTML =
'' +
'
No workers connected
' +
'
';
return;
}
wrap.innerHTML = '' + workers.map(workerRowHtml).join('') + '
';
}
/* -------------------------------------------------------------------
Live tick — update elapsed times on running rows and worker last-seen
------------------------------------------------------------------- */
function onTick() {
document.querySelectorAll('#dash-executions-wrap .dash-elapsed[data-started-at]').forEach(el => {
el.textContent = fmt.elapsed(el.getAttribute('data-started-at'));
});
document.querySelectorAll('#dash-workers-wrap .dash-worker-lastseen[data-last-heartbeat]').forEach(el => {
const v = el.getAttribute('data-last-heartbeat');
if (v) el.textContent = 'Last seen: ' + fmt.ago(v);
});
}
/* -------------------------------------------------------------------
Data loading
------------------------------------------------------------------- */
async function loadWorkers() {
try {
_workers = await Pulse.api.get('/api/workers') || [];
} catch (e) {
_workers = [];
console.error('[Pulse:dashboard] failed to load workers', e);
}
renderWorkers(_workers);
return _workers;
}
async function loadExecutions() {
try {
const data = await Pulse.api.get('/api/executions?limit=50&hide_internal=true');
_executions = (data && data.executions) || [];
} catch (e) {
_executions = [];
console.error('[Pulse:dashboard] failed to load executions', e);
}
renderExecutions(_executions);
return _executions;
}
async function loadRunningCount() {
try {
const data = await Pulse.api.get('/api/executions?status=running&limit=1');
return (data && data.total) || 0;
} catch (e) {
console.error('[Pulse:dashboard] failed to load running count', e);
return 0;
}
}
async function refresh() {
const [workers, , runningTotal] = await Promise.all([
loadWorkers(),
loadExecutions(),
loadRunningCount(),
]);
renderStats(workers, runningTotal);
}
function init() {
Pulse.actions.registerAll({
'dash:goto-workers': () => { window.location.href = '/workers'; },
'dash:goto-executions': () => { window.location.href = '/executions'; },
'dash:open-execution': (el) => {
const id = el.getAttribute('data-execution-id');
if (id) window.location.href = '/executions?open=' + encodeURIComponent(id);
},
});
Pulse.events.on('tick', onTick);
return refresh();
}
function onEvent(type) {
if (type === 'worker_update') {
Promise.all([loadWorkers(), loadRunningCount()]).then(([workers, rt]) => renderStats(workers, rt));
return true;
}
if (type === 'execution_started' || type === 'execution_status' ||
type === 'command_result' || type === 'workflow_result' ||
type === 'executions_bulk_deleted') {
Promise.all([loadExecutions(), loadRunningCount()]).then(([, rt]) => renderStats(_workers, rt));
return true;
}
return false;
}
Pulse.registerPage({ name: 'dashboard', init, refresh, onEvent });
})();