Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
129 lines
4.7 KiB
JavaScript
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);
|
|
};
|
|
}
|