App._metricsTab = 'overview';
App.renderMetrics = function() {
var metricsTab = App._metricsTab || 'overview';
var tabBar = '<div class="tabs" style="margin-bottom:1rem">'
+ '<button class="' + (metricsTab === 'overview' ? 'active' : '') + '" data-metrics-tab="overview">Overview</button>'
+ '<button class="' + (metricsTab === 'selection' ? 'active' : '') + '" data-metrics-tab="selection">Model Selection Log</button>'
+ '<button class="' + (metricsTab === 'requests' ? 'active' : '') + '" data-metrics-tab="requests">Request Log</button>'
+ '<button class="' + (metricsTab === 'traces' ? 'active' : '') + '" data-metrics-tab="traces">Pipeline Traces</button>'
+ '<button class="' + (metricsTab === 'flight' ? 'active' : '') + '" data-metrics-tab="flight">Flight Recorder</button>'
+ '<button class="' + (metricsTab === 'delegation' ? 'active' : '') + '" data-metrics-tab="delegation">Delegation Outcomes</button>'
+ '</div>';
if (metricsTab === 'traces') { return App.renderMetricsTraces(tabBar); }
if (metricsTab === 'flight') { return App.renderMetricsFlight(tabBar); }
if (metricsTab === 'delegation') { return App.renderMetricsDelegation(tabBar); }
return Promise.all([
api('/api/stats/costs'),
api('/api/stats/transactions?hours=24'),
api('/api/stats/capacity').catch(function() { return { providers: {} }; }),
api('/api/models/selections?limit=50').catch(function() { return { events: [] }; })
]).then(function(arr) {
stackedId = 0;
var costs = arr[0].costs || []; var txs = arr[1].transactions || []; var capacity = arr[2].providers || {};
var modelSelections = arr[3].events || [];
var providerCosts = {}; var providerTokens = {};
var activeProviders = PROVIDERS.slice();
var now = new Date(); var xLabels = [];
for (var i = 0; i < 24; i++) { var h = (now.getHours() - 23 + i + 24) % 24; xLabels.push(String(h).padStart(2, '0') + ':00'); }
// Discover all providers from actual data first
costs.forEach(function(c) { var p = (c.provider || '').toLowerCase(); if (p && activeProviders.indexOf(p) === -1) activeProviders.push(p); });
activeProviders.forEach(function(p) { providerCosts[p] = []; providerTokens[p] = []; for (var i = 0; i < 24; i++) { providerCosts[p].push(0); providerTokens[p].push(0); } });
costs.forEach(function(c) {
var p = (c.provider || '').toLowerCase();
if (!p || !providerCosts[p]) p = activeProviders[0] || 'unknown';
var costVal = Number(c.cost) || 0;
var tokVal = (Number(c.tokens_in) || 0) + (Number(c.tokens_out) || 0);
var bucket = 0;
if (c.created_at) {
var d = new Date(c.created_at);
if (!isNaN(d.getTime())) {
var hoursAgo = (now.getTime() - d.getTime()) / 3600000;
bucket = Math.max(0, Math.min(23, 23 - Math.floor(hoursAgo)));
}
}
providerCosts[p][bucket] += costVal;
providerTokens[p][bucket] += tokVal;
});
var FALLBACK_COLORS = ['#9baad6','#fb923c','#34d399','#f472b6','#facc15','#38bdf8','#c084fc','#a3e635'];
function pColor(p) { return PROVIDER_COLORS[p] || FALLBACK_COLORS[activeProviders.indexOf(p) % FALLBACK_COLORS.length] || '#9baad6'; }
var providerTotals = {}; var providerTotalTokens = {};
activeProviders.forEach(function(p) {
providerTotals[p] = providerCosts[p].reduce(function(a, b) { return a + b; }, 0);
providerTotalTokens[p] = providerTokens[p].reduce(function(a, b) { return a + b; }, 0);
});
// Filter to providers with actual activity for cleaner charts
var chartProviders = activeProviders.filter(function(p) { return providerTotals[p] > 0 || providerTotalTokens[p] > 0; });
if (chartProviders.length === 0) chartProviders = activeProviders.slice(0, 3);
var chartColors = {}; chartProviders.forEach(function(p) { chartColors[p] = pColor(p); });
var totalCost = activeProviders.reduce(function(s, p) { return s + providerTotals[p]; }, 0);
var totalTokens = activeProviders.reduce(function(s, p) { return s + providerTotalTokens[p]; }, 0);
var costChart = '<div class="card" style="margin-bottom:1rem;padding-bottom:0.75rem"><div class="card-title">Cost by Provider (24h)</div><div style="display:flex;align-items:baseline;gap:0.75rem;margin-bottom:0.25rem"><div class="card-value">$' + totalCost.toFixed(2) + '</div><span style="font-size:0.75rem;color:var(--muted)">' + costs.length + ' requests</span></div>' + renderStackedArea(providerCosts, chartProviders, chartColors, { height: 180, yAxis: true, xLabels: xLabels, yFormat: function(v) { return '$' + v.toFixed(3); } }) + '<div class="metrics-legend">';
chartProviders.forEach(function(p) { var pct = totalCost > 0 ? (providerTotals[p] / totalCost * 100).toFixed(0) : '0'; costChart += '<div class="metrics-legend-item"><div class="metrics-legend-dot" style="background:' + pColor(p) + '"></div>' + p + '<span class="metrics-legend-val">$' + providerTotals[p].toFixed(3) + ' (' + pct + '%)</span></div>'; });
costChart += '</div></div>';
var tokenChart = '<div class="card" style="margin-bottom:1rem;padding-bottom:0.75rem"><div class="card-title">Token Volume by Provider (24h)</div><div style="display:flex;align-items:baseline;gap:0.75rem;margin-bottom:0.25rem"><div class="card-value">' + Math.round(totalTokens).toLocaleString() + '</div><span style="font-size:0.75rem;color:var(--muted)">tokens total</span></div>' + renderStackedArea(providerTokens, chartProviders, chartColors, { height: 140, yAxis: true, xLabels: xLabels, yFormat: function(v) { return v >= 1000 ? (v / 1000).toFixed(1) + 'k' : Math.round(v).toString(); } }) + '<div class="metrics-legend">';
chartProviders.forEach(function(p) { tokenChart += '<div class="metrics-legend-item"><div class="metrics-legend-dot" style="background:' + pColor(p) + '"></div>' + p + '<span class="metrics-legend-val">' + Math.round(providerTotalTokens[p]).toLocaleString() + '</span></div>'; });
tokenChart += '</div></div>';
var avgCost = costs.length ? (totalCost / costs.length).toFixed(4) : '0';
var statGrid = '<div class="metrics-summary-grid"><div class="metrics-stat"><div class="metrics-stat-label">Avg / request</div><div class="metrics-stat-value">$' + avgCost + '</div></div>';
chartProviders.forEach(function(p) { statGrid += '<div class="metrics-stat"><div class="metrics-stat-label">' + p + ' share</div><div class="metrics-stat-value" style="color:' + pColor(p) + '">' + (totalCost > 0 ? (providerTotals[p] / totalCost * 100).toFixed(0) : '0') + '%</div></div>'; });
statGrid += '</div>';
var txRows = txs.map(function(t) { return '<tr><td class="card-mono">' + esc(t.tx_type || '') + '</td><td>' + esc((t.amount || '') + ' ' + (t.currency || '')) + '</td><td>' + esc(t.counterparty || '') + '</td><td>' + esc(t.created_at || '') + '</td></tr>'; }).join('');
var costRows = costs.slice(0, 20).map(function(c) { var pColor = PROVIDER_COLORS[(c.provider || '').toLowerCase()] || '#9baad6'; return '<tr><td class="card-mono">' + esc(truncate(c.id, 10)) + '</td><td>' + esc(c.model || '') + '</td><td><span style="color:' + pColor + '">' + esc(c.provider || '') + '</span></td><td>' + (c.tokens_in || 0) + '</td><td>' + (c.tokens_out || 0) + '</td><td>$' + Number(c.cost || 0).toFixed(6) + '</td><td>' + esc(c.created_at || '') + '</td></tr>'; }).join('');
var capNames = Object.keys(capacity).sort();
var capRows = capNames.map(function(name) {
var p = capacity[name] || {};
var headroom = Number(p.headroom || 0);
var pressure = p.sustained_hot ? '<span class="badge error">hot</span>' : (p.near_capacity ? '<span class="badge warning">near cap</span>' : '<span class="badge success">healthy</span>');
var tpm = p.tpm_limit ? (Math.round((p.token_utilization || 0) * 100) + '% (' + (p.tokens_used || 0).toLocaleString() + '/' + (p.tpm_limit || 0).toLocaleString() + ')') : 'n/a';
var rpm = p.rpm_limit ? (Math.round((p.request_utilization || 0) * 100) + '% (' + (p.requests_used || 0).toLocaleString() + '/' + (p.rpm_limit || 0).toLocaleString() + ')') : 'n/a';
return '<tr><td class="card-mono">' + esc(name) + '</td><td>' + Math.round(headroom * 100) + '%</td><td>' + pressure + '</td><td>' + esc(tpm) + '</td><td>' + esc(rpm) + '</td></tr>';
}).join('');
var capacityTable = '<p style="margin:1.25rem 0 0.5rem;color:var(--muted);font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em">Capacity & Headroom</p>'
+ '<div class="table-wrap" style="margin-bottom:1.25rem"><table><thead><tr><th>Provider</th><th>Headroom</th><th>State</th><th>TPM Utilization</th><th>RPM Utilization</th></tr></thead><tbody>'
+ (capRows || '<tr><td colspan="5" style="color:var(--muted)">No capacity limits configured.</td></tr>')
+ '</tbody></table></div>';
var selectionRows = modelSelections.map(function(ms) {
var strategy = ms.strategy || 'unknown';
var cands = (ms.candidates || []).length;
return '<tr>'
+ '<td class="card-mono">' + esc(truncate(ms.turn_id || '', 10)) + '</td>'
+ '<td class="card-mono">' + esc(ms.selected_model || '') + '</td>'
+ '<td>' + esc(strategy) + '</td>'
+ '<td>' + cands + '</td>'
+ '<td>' + esc(ms.complexity || '') + '</td>'
+ '<td>' + esc(ms.created_at || '') + '</td>'
+ '</tr>';
}).join('');
var selectionTable = '<p style="margin:1.25rem 0 0.5rem;color:var(--muted);font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em">Live Model Selection Log</p>'
+ '<div class="table-wrap" style="margin-bottom:1.25rem"><table><thead><tr><th>Turn</th><th>Selected Model</th><th>Strategy</th><th>Candidates</th><th>Complexity</th><th>Time</th></tr></thead><tbody>'
+ (selectionRows || '<tr><td colspan="6" style="color:var(--muted)">No model selection traces yet.</td></tr>')
+ '</tbody></table></div>';
var tabContent = '';
if (metricsTab === 'selection') {
tabContent = selectionTable;
} else if (metricsTab === 'requests') {
tabContent = '<p style="margin:1.25rem 0 0.5rem;color:var(--muted);font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em">Transactions</p><div class="table-wrap" style="margin-bottom:1.25rem"><table><thead><tr><th>Type</th><th>Amount</th><th>Counterparty</th><th>Time</th></tr></thead><tbody>' + txRows + '</tbody></table></div>'
+ '<p style="margin-bottom:0.5rem;color:var(--muted);font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em">Request log</p><div class="table-wrap"><table><thead><tr><th>ID</th><th>Model</th><th>Provider</th><th>In</th><th>Out</th><th>Cost</th><th>Time</th></tr></thead><tbody>' + costRows + '</tbody></table></div>';
} else {
tabContent = costChart + tokenChart + statGrid + capacityTable;
}
return tabBar + tabContent;
});
};
App.renderMetricsTraces = function(tabBar) {
var self = this;
var selectedSession = App._tracesSessionId || '';
return api('/api/sessions?limit=50').catch(function() { return { sessions: [] }; }).then(function(data) {
var sessions = data.sessions || [];
var sessionOpts = sessions.map(function(s) {
var sid = s.id || s.session_id || '';
var label = s.nickname || s.channel || truncate(sid, 12);
return '<option value="' + esc(sid) + '"' + (selectedSession === sid ? ' selected' : '') + '>' + esc(label) + '</option>';
}).join('');
var sessionSelector = '<div style="margin-bottom:1rem;display:flex;align-items:center;gap:0.75rem">'
+ '<label style="font-size:0.8125rem;color:var(--muted)">Session:</label>'
+ '<select id="traces-session-select" style="font-size:0.8125rem;padding:0.3rem 0.6rem;border:1px solid var(--border);border-radius:4px;background:var(--surface);color:var(--text)">'
+ '<option value="">-- select session --</option>'
+ sessionOpts
+ '</select>'
+ '</div>';
if (!selectedSession) {
return tabBar + sessionSelector + '<p style="color:var(--muted);font-size:0.875rem">Select a session to view pipeline traces.</p>';
}
return api('/api/observability/traces?session_id=' + encodeURIComponent(selectedSession) + '&limit=20').catch(function() { return { traces: [] }; }).then(function(td) {
var traces = td.traces || [];
var rows = traces.map(function(t) {
return '<tr class="traces-turn-row" data-turn-id="' + esc(t.turn_id || '') + '" style="cursor:pointer">'
+ '<td class="card-mono">' + esc(truncate(t.turn_id || '', 12)) + '</td>'
+ '<td>' + esc(t.channel || '') + '</td>'
+ '<td>' + (t.total_ms || 0) + 'ms</td>'
+ '<td>' + (t.stage_count || 0) + '</td>'
+ '<td>' + esc(t.created_at || '') + '</td>'
+ '<td><a href="#" class="trace-flow-link" data-flow-turn-id="' + esc(t.turn_id || '') + '" style="font-size:0.75rem;color:var(--accent);text-decoration:none" title="View decision flow">Flow</a></td>'
+ '</tr>'
+ '<tr class="traces-waterfall-row" id="wf-' + esc(t.turn_id || '') + '" style="display:none"><td colspan="6" style="padding:0.5rem 1rem 0.75rem;background:var(--surface-alt,var(--surface))"><div class="wf-content" style="color:var(--muted);font-size:0.8125rem">Loading\u2026</div></td></tr>';
}).join('');
var table = '<div class="table-wrap" style="margin-bottom:1.25rem"><table><thead><tr><th>Turn ID</th><th>Channel</th><th>Duration</th><th>Stages</th><th>Time</th><th></th></tr></thead><tbody>'
+ (rows || '<tr><td colspan="6" style="color:var(--muted)">No traces found for this session.</td></tr>')
+ '</tbody></table></div>';
return tabBar + sessionSelector + table;
});
});
};
App.renderMetricsFlight = function(tabBar) {
var turnId = App._flightTurnId || '';
var inputRow = '<div style="margin-bottom:1rem;display:flex;align-items:center;gap:0.75rem">'
+ '<label style="font-size:0.8125rem;color:var(--muted)">Turn ID:</label>'
+ '<input id="flight-turn-input" type="text" value="' + esc(turnId) + '" placeholder="Enter turn ID\u2026" style="font-size:0.8125rem;padding:0.3rem 0.6rem;border:1px solid var(--border);border-radius:4px;background:var(--surface);color:var(--text);width:22rem;font-family:var(--mono)">'
+ '<button class="btn" id="flight-load-btn" style="font-size:0.75rem;padding:0.3rem 0.7rem">Load</button>'
+ '</div>';
if (!turnId) {
return Promise.resolve(tabBar + inputRow + '<p style="color:var(--muted);font-size:0.875rem">Enter a turn ID to view the flight recorder trace.</p>');
}
return api('/api/traces/' + encodeURIComponent(turnId) + '/react').catch(function() { return { steps: [] }; }).then(function(data) {
var steps = data.steps || [];
function stepColor(type) {
if (type === 'ToolCall') return '#3b82f6';
if (type === 'Retrieval') return '#22c55e';
if (type === 'Guard') return '#f97316';
if (type === 'Normalization') return '#ef4444';
return '#9baad6';
}
function stepDetail(s) {
var parts = [];
if (s.tool_name) parts.push('tool: ' + esc(s.tool_name));
if (s.duration_ms != null) parts.push(s.duration_ms + 'ms');
if (s.success != null) parts.push(s.success ? 'ok' : 'fail');
if (s.candidates != null) parts.push('candidates: ' + s.candidates);
if (s.similarity != null) parts.push('sim: ' + Number(s.similarity).toFixed(3));
if (s.name) parts.push(esc(s.name));
if (s.fired != null) parts.push(s.fired ? 'fired' : 'pass');
if (s.action) parts.push(esc(s.action));
if (s.pattern) parts.push('pattern: ' + esc(truncate(s.pattern, 30)));
if (s.retry != null) parts.push('retry: ' + s.retry);
return parts.join(' \u00b7 ');
}
var timeline = steps.map(function(s, i) {
var type = s.step_type || s.type || 'Unknown';
var color = stepColor(type);
return '<div style="display:flex;align-items:flex-start;gap:0.75rem;margin-bottom:0.6rem">'
+ '<div style="min-width:3.5rem;text-align:right;font-size:0.6875rem;color:var(--muted);padding-top:0.2rem">' + (i + 1) + '</div>'
+ '<div style="width:2px;background:var(--border);align-self:stretch;min-height:1.5rem"></div>'
+ '<div style="flex:1;background:var(--surface);border:1px solid var(--border);border-left:3px solid ' + color + ';border-radius:4px;padding:0.35rem 0.65rem">'
+ '<div style="display:flex;align-items:center;gap:0.5rem">'
+ '<span style="font-size:0.75rem;font-weight:600;color:' + color + '">' + esc(type) + '</span>'
+ '<span style="font-size:0.75rem;color:var(--muted)">' + stepDetail(s) + '</span>'
+ '</div>'
+ '</div>'
+ '</div>';
}).join('');
var content = steps.length
? '<div style="padding:0.25rem 0">' + timeline + '</div>'
: '<p style="color:var(--muted);font-size:0.875rem">No ReAct steps recorded for this turn.</p>';
return tabBar + inputRow + content;
});
};
App.renderTraceFlow = function(turnId) {
return api('/api/traces/' + encodeURIComponent(turnId) + '/flow').then(function(data) {
var nodes = data.nodes || [];
var totalMs = data.total_ms || 0;
var header = '<div style="margin-bottom:1rem;display:flex;align-items:center;gap:0.75rem">'
+ '<button class="btn" id="flow-back-btn" style="font-size:0.75rem;padding:0.3rem 0.7rem">← Back</button>'
+ '<span style="font-size:0.8125rem;color:var(--muted)">Decision Flow for <span class="card-mono">' + esc(truncate(turnId, 16)) + '</span></span>'
+ '<span style="font-size:0.75rem;color:var(--muted);margin-left:auto;font-family:var(--font-mono)">' + totalMs + 'ms total</span>'
+ '</div>';
if (!nodes.length) {
return header + '<p style="color:var(--muted);font-size:0.875rem">No flow data for this turn.</p>';
}
// ── SVG Flow Graph ──────────────────────────────────────────
var nodeW = 180, nodeH = 44, gapY = 28, padX = 40, padY = 30;
var svgW = nodeW + padX * 2;
var svgH = nodes.length * (nodeH + gapY) - gapY + padY * 2;
var statusColor = function(s) {
if (s === 'pass' || s === 'executed' || s === 'Ok') return 'var(--success, #22c55e)';
if (s === 'skip' || s === 'Skipped') return 'var(--muted, #64748b)';
if (s === 'retry' || s === 'guard_fired') return 'var(--warning, #f59e0b)';
if (s === 'block' || s === 'fallback' || s === 'Error') return 'var(--error, #ef4444)';
return 'var(--accent, #c180ff)';
};
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + svgW + '" height="' + svgH
+ '" viewBox="0 0 ' + svgW + ' ' + svgH + '" style="display:block;margin:0 auto">';
// Arrow marker definition
svg += '<defs><marker id="flow-arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto">'
+ '<polygon points="0 0, 10 3.5, 0 7" fill="var(--border-ghost, #334155)"/></marker></defs>';
// Render edges (arrows between consecutive nodes)
for (var i = 0; i < nodes.length - 1; i++) {
var y1 = padY + i * (nodeH + gapY) + nodeH;
var y2 = padY + (i + 1) * (nodeH + gapY);
var cx = padX + nodeW / 2;
svg += '<line x1="' + cx + '" y1="' + y1 + '" x2="' + cx + '" y2="' + y2
+ '" stroke="var(--border-ghost, #334155)" stroke-width="2" marker-end="url(#flow-arrow)"/>';
}
// Render nodes
nodes.forEach(function(n, idx) {
var x = padX, y = padY + idx * (nodeH + gapY);
var status = n.status || 'executed';
var borderColor = statusColor(status);
var label = n.label || n.id || 'stage';
var dur = (n.duration_ms || 0) + 'ms';
svg += '<g class="flow-svg-node" data-flow-idx="' + idx + '" style="cursor:pointer">'
+ '<rect x="' + x + '" y="' + y + '" width="' + nodeW + '" height="' + nodeH
+ '" rx="8" fill="var(--surface-2, #1e293b)" stroke="' + borderColor + '" stroke-width="2"/>'
+ '<text x="' + (x + 12) + '" y="' + (y + 18) + '" fill="var(--text, #e2e8f0)" font-size="12" font-weight="600" font-family="var(--font, sans-serif)">'
+ esc(label.length > 20 ? label.substring(0, 18) + '..' : label) + '</text>'
+ '<text x="' + (x + 12) + '" y="' + (y + 34) + '" fill="var(--muted, #94a3b8)" font-size="10" font-family="var(--font-mono, monospace)">'
+ esc(dur) + ' · ' + esc(status) + '</text>'
+ '<circle cx="' + (x + nodeW - 14) + '" cy="' + (y + nodeH / 2)
+ '" r="5" fill="' + borderColor + '"/>'
+ '</g>';
});
svg += '</svg>';
// ── Floating detail popover (click to show) ─────────────────
var popover = '<div id="flow-popover" style="display:none;position:absolute;z-index:100;background:var(--surface-3,#1a1a2e);border:1px solid var(--accent);border-radius:var(--radius,8px);padding:0.75rem 1rem;max-width:420px;box-shadow:0 8px 32px rgba(0,0,0,0.4);font-size:0.8125rem;color:var(--text);pointer-events:auto">'
+ '<div id="flow-popover-content"></div></div>';
// Store node data for click handler
var flowContainer = '<div id="flow-svg-container" style="position:relative;overflow-x:auto">'
+ svg + popover + '</div>'
+ '<script type="application/json" id="flow-node-data">' + JSON.stringify(nodes.map(function(n) {
return { id: n.id, label: n.label, status: n.status, duration_ms: n.duration_ms, detail: n.detail };
})) + '</' + 'script>';
return header + flowContainer;
}).catch(function(err) {
return '<p style="color:var(--error)">Failed to load flow: ' + esc(err.message || String(err)) + '</p>';
});
};
App.renderMetricsDelegation = function(tabBar) {
return Promise.all([
api('/api/observability/delegation/outcomes?limit=20').catch(function() { return { outcomes: [] }; }),
api('/api/observability/delegation/stats?hours=24').catch(function() { return {}; })
]).then(function(arr) {
var outcomes = arr[0].outcomes || [];
var stats = arr[1] || {};
function qualColor(q) {
var v = Number(q) || 0;
return v > 0.7 ? '#22c55e' : v >= 0.4 ? '#facc15' : '#ef4444';
}
var totalDels = stats.total_delegations || outcomes.length;
var successRate = stats.success_rate != null ? (Number(stats.success_rate) * 100).toFixed(0) + '%' : 'n/a';
var avgQuality = stats.avg_quality != null ? Number(stats.avg_quality).toFixed(3) : 'n/a';
var summaryRow = '<div class="metrics-summary-grid" style="margin-bottom:1rem">'
+ '<div class="metrics-stat"><div class="metrics-stat-label">Total delegations</div><div class="metrics-stat-value">' + totalDels + '</div></div>'
+ '<div class="metrics-stat"><div class="metrics-stat-label">Success rate (24h)</div><div class="metrics-stat-value">' + esc(successRate) + '</div></div>'
+ '<div class="metrics-stat"><div class="metrics-stat-label">Avg quality (24h)</div><div class="metrics-stat-value">' + esc(avgQuality) + '</div></div>'
+ '</div>';
var rows = outcomes.map(function(o) {
var agents = '';
try { var ag = JSON.parse(o.assigned_agents_json || '[]'); agents = ag.join(', '); } catch(e) { agents = o.assigned_agents_json || ''; }
var q = Number(o.quality_score) || 0;
return '<tr>'
+ '<td>' + esc(truncate(o.task_description || '', 60)) + '</td>'
+ '<td>' + (o.subtask_count || 0) + '</td>'
+ '<td class="card-mono" style="font-size:0.75rem">' + esc(truncate(agents, 40)) + '</td>'
+ '<td>' + (o.duration_ms || 0) + 'ms</td>'
+ '<td><span style="color:' + qualColor(q) + ';font-weight:600">' + q.toFixed(3) + '</span></td>'
+ '<td>' + (o.retry_count || 0) + '</td>'
+ '<td>' + esc(o.created_at || '') + '</td>'
+ '</tr>';
}).join('');
var table = '<div class="table-wrap"><table><thead><tr><th>Task</th><th>Subtasks</th><th>Agents</th><th>Duration</th><th>Quality</th><th>Retries</th><th>Time</th></tr></thead><tbody>'
+ (rows || '<tr><td colspan="7" style="color:var(--muted)">No delegation outcomes recorded yet.</td></tr>')
+ '</tbody></table></div>';
return tabBar + summaryRow + table;
});
};