/* ===================================================================== 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 = '' + '' + '' + manual.map(executionRowHtml).join('') + '' + '
StatusNameStarted ByStarted AtElapsed
'; } /* ------------------------------------------------------------------- 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 }); })();