Files
pulse/public/assets/pages/dashboard.js
T

224 lines
8.5 KiB
JavaScript
Raw Normal View History

/* =====================================================================
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 (
'<tr data-action="dash:open-execution" data-execution-id="' + esc(e.id) + '">' +
'<td data-label="Status"><span class="' + statusClass + '">' + statusText + '</span></td>' +
'<td data-label="Name">' + name + '</td>' +
'<td data-label="Started By">' + startedBy + '</td>' +
'<td data-label="Started At">' + esc(startedAt) + '</td>' +
'<td data-label="Elapsed" class="dash-elapsed"' +
(isRunning ? ' data-started-at="' + esc(e.started_at) + '"' : '') + '>' + elapsed + '</td>' +
'</tr>'
);
}
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 =
'<div class="lt-empty-state lt-empty-state--sm">' +
'<div class="lt-empty-state-title">No executions yet</div>' +
'</div>';
return;
}
wrap.innerHTML =
'<table class="lt-table lt-table-sm lt-table-responsive">' +
'<thead><tr><th>Status</th><th>Name</th><th>Started By</th><th>Started At</th><th>Elapsed</th></tr></thead>' +
'<tbody>' + manual.map(executionRowHtml).join('') + '</tbody>' +
'</table>';
}
/* -------------------------------------------------------------------
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 =
'<span>CPU: ' + esc(meta.cpus || '?') + ' cores</span>' +
'<span>RAM: ' + fmt.bytes(meta.totalMem - meta.freeMem) + ' / ' + fmt.bytes(meta.totalMem) + '</span>' +
'<span>Tasks: ' + esc(meta.activeTasks || 0) + '/' + esc(meta.maxConcurrentTasks || 0) + '</span>';
}
return (
'<div class="dash-worker-row" data-worker-id="' + esc(w.id) + '">' +
'<div class="dash-worker-main">' +
'<span class="lt-dot ' + dotClass + '"></span>' +
'<span class="dash-worker-name">' + esc(w.name) + '</span>' +
'<span class="' + statusClass + '">' + esc(String(w.status || '').toUpperCase()) + '</span>' +
'<span class="dash-worker-lastseen" data-last-heartbeat="' + esc(w.last_heartbeat || '') + '">Last seen: ' + esc(lastSeen) + '</span>' +
'</div>' +
(stats ? '<div class="dash-worker-stats">' + stats + '</div>' : '') +
'</div>'
);
}
function renderWorkers(workers) {
const wrap = document.getElementById('dash-workers-wrap');
if (!wrap) return;
if (workers.length === 0) {
wrap.innerHTML =
'<div class="lt-empty-state lt-empty-state--sm">' +
'<div class="lt-empty-state-title">No workers connected</div>' +
'</div>';
return;
}
wrap.innerHTML = '<div class="dash-worker-list">' + workers.map(workerRowHtml).join('') + '</div>';
}
/* -------------------------------------------------------------------
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
------------------------------------------------------------------- */
/* Toast a load failure at most once per outage, so the 30 s auto-refresh
does not stack a toast every cycle while the API is down. */
let _loadFailed = false;
function reportLoadError(what, e) {
console.error('[Pulse:dashboard] failed to load ' + what, e);
if (_loadFailed) return;
_loadFailed = true;
if (Pulse.toast) Pulse.toast.error((e && e.message) || ('Failed to load ' + what));
}
async function loadWorkers() {
try {
_workers = await Pulse.api.get('/api/workers') || [];
_loadFailed = false;
} catch (e) {
_workers = [];
reportLoadError('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 = [];
reportLoadError('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 });
})();