Add live metrics dashboard to progress panel

8 real-time metrics in the progress panel:
- Elapsed time (updates every 500ms)
- Models active/total (tracks unique models as they respond)
- Responses received (count)
- Estimated tokens (~chars/4)
- Data received (formatted KB)
- SSE events (total protocol events)
- Errors (turns red if > 0)
- Heartbeats (keepalive count)

Metrics update every 500ms during run. On completion, all
metric values turn green. Magenta/purple theme for metric
values, micro labels underneath.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-03-26 01:55:29 -05:00
parent c507ba1016
commit 1ac7a436e6

View File

@ -662,6 +662,13 @@ HTML = r"""
.progress-detail { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: #e0b0ff; display: flex; justify-content: space-between; }
.progress-detail .prog-substep { max-width: 70%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.progress-detail .prog-stats { color: #c084fc; opacity: 0.7; }
.prog-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(217,70,239,0.15); }
.prog-metric { text-align: center; padding: 4px 2px; }
.prog-metric .mv { font-family: 'JetBrains Mono', monospace; font-size: 14px; font-weight: 700; color: #f0abfc; line-height: 1; }
.prog-metric .ml { font-family: 'JetBrains Mono', monospace; font-size: 7px; text-transform: uppercase; letter-spacing: 1.5px; color: rgba(217,70,239,0.5); margin-top: 3px; }
.prog-metric.highlight .mv { color: #4ade80; }
.prog-metric.warn .mv { color: #f59e0b; }
.prog-metric.err .mv { color: #e05252; }
.phase-label { font-family: 'JetBrains Mono', monospace; font-size: 9px; text-transform: uppercase; letter-spacing: 2px; color: #4ade80; padding: 12px 0 6px; opacity: 0.8; display: flex; align-items: center; gap: 8px; }
.phase-label::before { content: ''; flex: 0 0 12px; height: 2px; background: #4ade80; opacity: 0.6; }
.phase-label::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, rgba(74,222,128,0.3), transparent); }
@ -1281,6 +1288,10 @@ let _runStartTime = 0;
let _runTimer = null;
let _runEventCount = 0;
let _runResponseCount = 0;
let _runTotalChars = 0;
let _runModelsUsed = new Set();
let _runErrors = 0;
let _runKeepAlives = 0;
function formatElapsed(ms) {
const s = Math.floor(ms / 1000);
@ -1288,66 +1299,145 @@ function formatElapsed(ms) {
return Math.floor(s/60) + 'm ' + (s%60) + 's';
}
function updateProgressTime() {
const el = document.getElementById('prog-time');
function formatBytes(chars) {
if (chars < 1000) return chars + ' ch';
if (chars < 100000) return (chars/1000).toFixed(1) + 'K';
return (chars/1000).toFixed(0) + 'K';
}
function estimateTokens(chars) {
var t = Math.round(chars / 4);
if (t < 1000) return t.toString();
return (t/1000).toFixed(1) + 'K';
}
function updateProgressMetrics() {
var el = document.getElementById('prog-time');
if (el && _runStartTime) el.textContent = formatElapsed(Date.now() - _runStartTime);
const ev = document.getElementById('prog-events');
if (ev) ev.textContent = _runEventCount + ' events / ' + _runResponseCount + ' responses';
var m;
m = document.getElementById('pm-elapsed');
if (m) m.textContent = formatElapsed(Date.now() - _runStartTime);
m = document.getElementById('pm-models');
if (m) m.textContent = _runModelsUsed.size;
m = document.getElementById('pm-responses');
if (m) m.textContent = _runResponseCount;
m = document.getElementById('pm-tokens');
if (m) m.textContent = '~' + estimateTokens(_runTotalChars);
m = document.getElementById('pm-data');
if (m) m.textContent = formatBytes(_runTotalChars);
m = document.getElementById('pm-events');
if (m) m.textContent = _runEventCount;
m = document.getElementById('pm-errors');
if (m) { m.textContent = _runErrors; m.parentNode.className = _runErrors > 0 ? 'prog-metric err' : 'prog-metric'; }
m = document.getElementById('pm-heartbeat');
if (m) m.textContent = _runKeepAlives;
}
async function runTeam() {
const config = buildConfig();
var config = buildConfig();
if (!config) return;
const btn = document.getElementById('run-btn');
var btn = document.getElementById('run-btn');
btn.disabled = true; btn.textContent = 'Running...';
const output = document.getElementById('output');
var output = document.getElementById('output');
_runStartTime = Date.now();
_runEventCount = 0;
_runResponseCount = 0;
const progEl = document.createElement('div');
_runTotalChars = 0;
_runModelsUsed = new Set();
_runErrors = 0;
_runKeepAlives = 0;
// Count models in config
var cfgModels = config.models ? config.models.length : 0;
var totalModels = cfgModels;
if (config.synthesizer) totalModels++;
if (config.scout) totalModels++;
if (config.checker) totalModels++;
if (config.judge) totalModels++;
var progEl = document.createElement('div');
progEl.className = 'progress-panel';
progEl.id = 'run-progress';
progEl.textContent = '';
const header = document.createElement('div');
// Header row
var header = document.createElement('div');
header.className = 'progress-header';
const modeLabel = document.createElement('span');
var modeLabel = document.createElement('span');
modeLabel.className = 'prog-mode';
modeLabel.textContent = currentMode;
const timeLabel = document.createElement('span');
var timeLabel = document.createElement('span');
timeLabel.className = 'prog-time';
timeLabel.id = 'prog-time';
timeLabel.textContent = '0s';
header.appendChild(modeLabel);
header.appendChild(timeLabel);
progEl.appendChild(header);
const track = document.createElement('div');
// Progress bar
var track = document.createElement('div');
track.className = 'progress-track';
const fill = document.createElement('div');
var fill = document.createElement('div');
fill.className = 'progress-fill';
fill.id = 'prog-fill';
fill.style.width = '2%';
track.appendChild(fill);
progEl.appendChild(track);
const stepsDiv = document.createElement('div');
// Step indicators
var stepsDiv = document.createElement('div');
stepsDiv.className = 'progress-steps';
stepsDiv.id = 'prog-steps';
progEl.appendChild(stepsDiv);
const detail = document.createElement('div');
// Substep detail
var detail = document.createElement('div');
detail.className = 'progress-detail';
const substep = document.createElement('span');
var substep = document.createElement('span');
substep.className = 'prog-substep';
substep.id = 'prog-substep';
substep.textContent = 'Initializing...';
const stats = document.createElement('span');
var stats = document.createElement('span');
stats.className = 'prog-stats';
stats.id = 'prog-events';
stats.textContent = '0 events';
stats.textContent = '';
detail.appendChild(substep);
detail.appendChild(stats);
progEl.appendChild(detail);
// Metrics grid
var metrics = document.createElement('div');
metrics.className = 'prog-metrics';
metrics.id = 'prog-metrics';
var metricDefs = [
{id:'pm-elapsed', label:'Elapsed', val:'0s'},
{id:'pm-models', label:'Models', val:'0/' + totalModels},
{id:'pm-responses', label:'Responses', val:'0'},
{id:'pm-tokens', label:'Est. Tokens', val:'~0'},
{id:'pm-data', label:'Data Recv', val:'0 ch'},
{id:'pm-events', label:'SSE Events', val:'0'},
{id:'pm-errors', label:'Errors', val:'0'},
{id:'pm-heartbeat', label:'Heartbeats', val:'0'}
];
metricDefs.forEach(function(md) {
var box = document.createElement('div');
box.className = 'prog-metric';
var v = document.createElement('div');
v.className = 'mv';
v.id = md.id;
v.textContent = md.val;
var l = document.createElement('div');
l.className = 'ml';
l.textContent = md.label;
box.appendChild(v);
box.appendChild(l);
metrics.appendChild(box);
});
progEl.appendChild(metrics);
output.textContent = '';
output.appendChild(progEl);
_runTimer = setInterval(updateProgressTime, 1000);
_runTimer = setInterval(updateProgressMetrics, 500);
try {
const resp = await fetch('/api/run', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(config) });
if (!resp.ok) {
@ -1365,6 +1455,7 @@ async function runTeam() {
buffer = lines.pop();
for (const line of lines) {
if (line.startsWith('data: ')) { try { _runEventCount++; handleEvent(JSON.parse(line.slice(6))); } catch(e) {} }
else if (line.indexOf('keepalive') >= 0) { _runKeepAlives++; }
}
}
} catch(e) {
@ -1376,16 +1467,20 @@ async function runTeam() {
output.appendChild(errDiv);
}
clearInterval(_runTimer);
const prog = document.getElementById('run-progress');
updateProgressMetrics();
var prog = document.getElementById('run-progress');
if (prog) {
prog.classList.add('done');
const fillEl = document.getElementById('prog-fill');
var fillEl = document.getElementById('prog-fill');
if (fillEl) { fillEl.style.width = '100%'; fillEl.style.boxShadow = '0 0 20px rgba(74,222,128,0.5)'; fillEl.style.background = 'linear-gradient(90deg, #4ade80, #22d3ee)'; }
const sub = document.getElementById('prog-substep');
if (sub) sub.textContent = 'Complete — ' + formatElapsed(Date.now() - _runStartTime) + '' + _runResponseCount + ' responses';
const allSteps = prog.querySelectorAll('.progress-step');
var sub = document.getElementById('prog-substep');
if (sub) sub.textContent = 'Complete — ' + formatElapsed(Date.now() - _runStartTime) + '' + _runResponseCount + ' responses — ~' + estimateTokens(_runTotalChars) + ' tokens';
var allSteps = prog.querySelectorAll('.progress-step');
allSteps.forEach(function(s) { s.className = 'progress-step done'; });
setTimeout(function() { if (prog.parentNode) { prog.style.opacity = '0.5'; } }, 5000);
// Turn metric values green on completion
var mvs = prog.querySelectorAll('.prog-metric');
mvs.forEach(function(m) { if (!m.classList.contains('err') || _runErrors === 0) m.classList.add('highlight'); });
setTimeout(function() { if (prog.parentNode) { prog.style.opacity = '0.6'; } }, 8000);
}
btn.disabled = false; btn.textContent = 'Run Team';
}
@ -1440,6 +1535,9 @@ function handleEvent(evt) {
if (evt.type === 'done') { const bar = output.querySelector('.status-bar'); if (bar) bar.remove(); return; }
if (evt.type === 'response') {
_runResponseCount++;
_runTotalChars += (evt.text || '').length;
if (evt.model) _runModelsUsed.add(evt.model);
if (evt.role === 'error') _runErrors++;
const bar = output.querySelector('.status-bar'); if (bar) bar.remove();
// Phase labels show when role changes
const role = evt.role || 'response';