This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/workflow-monitor.js
Jeffrey Smith bf8082e69f Changeset 0.35.0 (#209)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-20 09:59:53 +00:00

129 lines
4.7 KiB
JavaScript

// workflow-monitor.js — v0.35.0 Workflow Monitoring Dashboard
//
// Admin monitoring tab showing active instances, stage funnels,
// SLA status indicators, and stale instance detection.
export function mountMonitorTab(container, { basePath }) {
container.innerHTML = `
<div class="wf-monitor">
<div class="wf-monitor-header" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3 style="font-size:16px;font-weight:600">Workflow Monitor</h3>
<button id="monRefresh" class="btn btn-sm btn-outline">Refresh</button>
</div>
<div id="monInstances" style="margin-bottom:24px">Loading…</div>
<div id="monStale" style="margin-bottom:24px"></div>
</div>
`;
let refreshTimer = null;
async function loadInstances() {
try {
const resp = await fetch(`${basePath}/api/v1/admin/workflows/monitor/instances`);
if (!resp.ok) throw new Error(resp.statusText);
const { data } = await resp.json();
renderInstances(data);
} catch (e) {
document.getElementById('monInstances').textContent = 'Failed to load: ' + e.message;
}
}
async function loadStale() {
try {
const resp = await fetch(`${basePath}/api/v1/admin/workflows/monitor/stale`);
if (!resp.ok) return;
const { data } = await resp.json();
renderStale(data);
} catch (_) {}
}
function renderInstances(instances) {
const el = document.getElementById('monInstances');
if (!instances.length) {
el.innerHTML = '<p style="color:var(--text-3)">No active workflow instances.</p>';
return;
}
let html = '<table style="width:100%;border-collapse:collapse;font-size:13px">';
html += '<thead><tr style="border-bottom:2px solid var(--border);text-align:left">';
html += '<th style="padding:8px">Workflow</th><th style="padding:8px">Stage</th>';
html += '<th style="padding:8px">Age</th><th style="padding:8px">SLA</th>';
html += '</tr></thead><tbody>';
for (const inst of instances) {
const age = formatDuration(inst.age_seconds);
const sla = renderSLA(inst);
html += `<tr style="border-bottom:1px solid var(--border)">`;
html += `<td style="padding:8px">${esc(inst.workflow_name)}<br><span style="font-size:11px;color:var(--text-3)">${esc(inst.channel_title)}</span></td>`;
html += `<td style="padding:8px">${esc(inst.stage_name)} <span style="color:var(--text-3)">(${inst.current_stage})</span></td>`;
html += `<td style="padding:8px">${age}</td>`;
html += `<td style="padding:8px">${sla}</td>`;
html += '</tr>';
}
html += '</tbody></table>';
el.innerHTML = html;
}
function renderSLA(inst) {
if (!inst.sla_seconds) return '<span style="color:var(--text-3)">—</span>';
if (inst.sla_breached) {
return '<span style="color:var(--danger,#e74c3c);font-weight:600">BREACHED</span>';
}
if (inst.sla_remaining_seconds != null) {
const remaining = inst.sla_remaining_seconds;
const pct = Math.max(0, remaining / inst.sla_seconds);
const color = pct > 0.5 ? 'var(--success,#2ecc71)' : pct > 0.2 ? 'var(--warning,#f39c12)' : 'var(--danger,#e74c3c)';
return `<span style="color:${color}">${formatDuration(remaining)}</span>`;
}
return '—';
}
function renderStale(instances) {
const el = document.getElementById('monStale');
if (!instances.length) {
el.innerHTML = '';
return;
}
let html = '<h4 style="font-size:14px;color:var(--danger,#e74c3c);margin-bottom:8px">Stale Instances (' + instances.length + ')</h4>';
html += '<ul style="list-style:none;padding:0">';
for (const inst of instances) {
html += `<li style="padding:4px 0;font-size:13px">${esc(inst.workflow_name)}${esc(inst.stage_name)} (${formatDuration(inst.age_seconds)} old)</li>`;
}
html += '</ul>';
el.innerHTML = html;
}
function formatDuration(seconds) {
if (seconds < 60) return seconds + 's';
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
return Math.floor(seconds / 86400) + 'd ' + Math.floor((seconds % 86400) / 3600) + 'h';
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
// Initial load
loadInstances();
loadStale();
// Auto-refresh every 30s
refreshTimer = setInterval(() => {
loadInstances();
loadStale();
}, 30000);
document.getElementById('monRefresh')?.addEventListener('click', () => {
loadInstances();
loadStale();
});
// Return cleanup function
return () => {
if (refreshTimer) clearInterval(refreshTimer);
};
}