// Metrics tab - success rate, duration, throughput, DORA, costs, stage performance import { store } from "../core/state"; import { escapeHtml, fmtNum, formatDuration, animateValue, } from "../core/helpers"; import { renderSVGDonut } from "../components/charts/donut"; import { renderSVGBarChart } from "../components/charts/bar"; import { renderSparkline, renderSVGLineChart, } from "../components/charts/sparkline"; import { renderDoraGrades } from "../components/charts/pipeline-rail"; import { STAGES, STAGE_COLORS, STAGE_HEX } from "../design/tokens"; import * as api from "../core/api"; import type { FleetState, View, MetricsData } from "../types/api"; function fetchMetrics(): void { api .fetchMetricsHistory() .then((data) => { store.set("metricsCache", data); renderMetrics(data); }) .catch((err) => { const el = document.getElementById("metrics-grid"); if (el) el.innerHTML = `

Failed to load metrics: ${escapeHtml(String(err))}

`; }); } function renderMetrics(data: MetricsData): void { const firstRender = store.get("firstRender"); // Success rate donut const rate = data.success_rate ?? 0; const donutWrap = document.getElementById("metric-donut-wrap"); if (donutWrap) donutWrap.innerHTML = renderSVGDonut(rate); // Avg duration const avgDurEl = document.getElementById("metric-avg-duration"); if (avgDurEl) avgDurEl.textContent = formatDuration(data.avg_duration_s); // Throughput const tp = data.throughput_per_hour ?? 0; const tpEl = document.getElementById("metric-throughput"); if (tpEl) tpEl.textContent = tp.toFixed(2); // Totals const totalCompleted = data.total_completed ?? 0; const totalFailed = data.total_failed ?? 0; const tcEl = document.getElementById("metric-total-completed"); if (tcEl) { if (firstRender && totalCompleted > 0) animateValue(tcEl, 0, totalCompleted, 800, ""); else tcEl.textContent = fmtNum(totalCompleted); } const failedEl = document.getElementById("metric-total-failed"); if (failedEl) { failedEl.textContent = fmtNum(totalFailed) + " failed"; failedEl.style.color = totalFailed > 0 ? "var(--rose)" : ""; } // Stage breakdown renderStageBreakdown(data.stage_durations || {}); // Daily chart renderDailyChart(data.daily_counts || []); // DORA grades const doraContainer = document.getElementById("dora-grades-container"); if (doraContainer && data.dora_grades) { doraContainer.innerHTML = renderDoraGrades( data.dora_grades as unknown as Record< string, { grade: string; value: number; unit: string } >, ); doraContainer.style.display = ""; } else if (doraContainer) { doraContainer.style.display = "none"; } // Cost breakdown/trend/context efficiency if (document.getElementById("cost-breakdown-container")) renderCostBreakdown(); if (document.getElementById("cost-trend-container")) renderCostTrend(); if (document.getElementById("context-efficiency-container")) renderContextEfficiency(); if (document.getElementById("dora-trend-container")) renderDoraTrend(); if (document.getElementById("stage-performance-container")) renderStagePerformance(); if (document.getElementById("bottleneck-alert-container")) renderBottleneckAlert(); if (document.getElementById("throughput-trend-container")) renderThroughputTrend(); if (document.getElementById("capacity-forecast-container")) renderCapacityForecast(); } function renderStageBreakdown(stageDurations: Record): void { const container = document.getElementById("stage-breakdown"); if (!container) return; const keys = Object.keys(stageDurations); if (keys.length === 0) { container.innerHTML = '

No data

'; return; } let maxVal = 0; for (const k of keys) { if (stageDurations[k] > maxVal) maxVal = stageDurations[k]; } if (maxVal === 0) maxVal = 1; let html = ""; keys.forEach((stage, i) => { const val = stageDurations[stage]; const pct = (val / maxVal) * 100; const colorIdx = i % STAGE_COLORS.length; html += `
` + `${escapeHtml(stage)}` + `
` + `${formatDuration(val)}
`; }); container.innerHTML = html; } function renderDailyChart(dailyCounts: any[]): void { const container = document.getElementById("daily-chart"); if (!container) return; if (!dailyCounts || dailyCounts.length === 0) { container.innerHTML = '

No data

'; return; } container.innerHTML = renderSVGBarChart(dailyCounts); } function renderCostBreakdown(): void { const container = document.getElementById("cost-breakdown-container"); if (!container) return; api .fetchCostBreakdown() .then((data) => { store.set("costBreakdownCache", data as any); let html = ""; if (data.by_model) { html += '
'; const modelColors: Record = { opus: "#7c3aed", sonnet: "#00d4ff", haiku: "#4ade80", }; const models = Object.keys(data.by_model); let maxModel = 0; models.forEach((m) => { if (data.by_model![m] > maxModel) maxModel = data.by_model![m]; }); if (maxModel === 0) maxModel = 1; models.forEach((m) => { const val = data.by_model![m]; const pct = (val / maxModel) * 100; const color = modelColors[m.toLowerCase()] || "#5a6d8a"; html += `
${escapeHtml(m)}` + `
` + `$${val.toFixed(2)}
`; }); html += "
"; } if (data.by_stage) { html += '
'; const stages = Object.keys(data.by_stage); let maxStage = 0; stages.forEach((s) => { if (data.by_stage![s] > maxStage) maxStage = data.by_stage![s]; }); if (maxStage === 0) maxStage = 1; stages.forEach((s) => { const val = data.by_stage![s]; const pct = (val / maxStage) * 100; const barColor = (STAGE_HEX as Record)[s] || "#5a6d8a"; html += `
${escapeHtml(s)}` + `
` + `$${val.toFixed(2)}
`; }); html += "
"; } if (data.by_issue?.length) { html += '
'; html += ''; const sorted = [...data.by_issue].sort( (a, b) => (b.cost || 0) - (a.cost || 0), ); sorted.forEach((item) => { html += ``; }); html += "
IssueCost
#${item.issue}$${(item.cost || 0).toFixed(2)}
"; } if (data.budget != null && data.spent != null) { const budgetPct = data.budget > 0 ? Math.min((data.spent / data.budget) * 100, 100) : 0; const budgetClass = budgetPct >= 80 ? "cost-over" : budgetPct >= 60 ? "cost-warn" : "cost-ok"; html += `
` + `
` + `$${data.spent.toFixed(2)} / $${data.budget.toFixed(2)} (${budgetPct.toFixed(0)}%)
`; } container.innerHTML = html || '

No cost data

'; }) .catch((err) => { container.innerHTML = `

Failed to load: ${escapeHtml(String(err))}

`; }); } function renderCostTrend(): void { const container = document.getElementById("cost-trend-container"); if (!container) return; api .fetchCostTrend() .then((data) => { const points = data.points || []; if (points.length === 0) { container.innerHTML = '

No trend data

'; return; } container.innerHTML = renderSVGLineChart( points, "cost", "#00d4ff", 300, 100, ); }) .catch((err) => { container.innerHTML = `

Failed to load: ${escapeHtml(String(err))}

`; }); } function renderContextEfficiency(): void { const container = document.getElementById("context-efficiency-container"); if (!container) return; api .fetchContextEfficiency() .then((data) => { if (!data.total_iterations) { container.innerHTML = '

No context efficiency data

'; return; } const utilPct = Math.min(data.avg_utilization, 100); const utilClass = utilPct >= 90 ? "ctx-eff-high" : utilPct >= 60 ? "ctx-eff-mid" : "ctx-eff-low"; const trimPct = Math.min(data.avg_trim_ratio, 100); let html = 'CONTEXT EFFICIENCY' + '
'; // Budget utilization gauge html += '
' + 'Budget Utilization' + `
` + `${data.avg_utilization.toFixed(1)}%` + "
"; // Trim ratio html += '
' + 'Avg Trim Ratio' + `
` + `${data.avg_trim_ratio.toFixed(1)}%` + "
"; // Chars saved const savedK = Math.round(data.total_discarded_chars / 1000); const totalK = Math.round(data.total_raw_chars / 1000); html += '
' + 'Chars Discarded' + `${fmtNum(savedK)}K` + `of ${fmtNum(totalK)}K generated` + "
"; // Trim events html += '
' + 'Trim Events' + `${data.trim_events}` + `of ${data.total_iterations} iterations` + "
"; html += "
"; container.innerHTML = html; }) .catch((err) => { container.innerHTML = `

Failed to load: ${escapeHtml(String(err))}

`; }); } function renderDoraTrend(): void { const container = document.getElementById("dora-trend-container"); if (!container) return; api .fetchDoraTrend() .then((data) => { const metrics = [ { key: "deploy_freq", label: "Deploy Freq", color: "#00d4ff" }, { key: "lead_time", label: "Lead Time", color: "#0066ff" }, { key: "cfr", label: "Change Fail Rate", color: "#f43f5e" }, { key: "mttr", label: "MTTR", color: "#4ade80" }, ]; let html = '
'; for (const m of metrics) { const points = data[m.key] || []; html += `
${escapeHtml(m.label)}`; html += points.length > 0 ? renderSparkline( points as Array, m.color, 120, 30, ) : '\u2014'; html += "
"; } html += "
"; container.innerHTML = html; }) .catch((err) => { container.innerHTML = `

Failed to load: ${escapeHtml(String(err))}

`; }); } function renderStagePerformance(): void { const container = document.getElementById("stage-performance-container"); if (!container) return; api .fetchStagePerformance() .then((data) => { const stages = data.stages || []; if (stages.length === 0) { container.innerHTML = '

No stage performance data

'; return; } let html = ''; for (const s of stages) { let trendArrow = ""; if (s.trend_pct != null) { if (s.trend_pct > 5) trendArrow = `\u2191 ${s.trend_pct.toFixed(0)}%`; else if (s.trend_pct < -5) trendArrow = `\u2193 ${Math.abs(s.trend_pct).toFixed(0)}%`; else trendArrow = '\u2192'; } html += `` + ``; } html += "
StageAvgMinMaxCountTrend
${escapeHtml(s.name || s.stage || "")}${formatDuration(s.avg_s)}${formatDuration(s.min_s)}${formatDuration(s.max_s)}${s.count || 0}${trendArrow}
"; container.innerHTML = html; }) .catch((err) => { container.innerHTML = `

Failed to load: ${escapeHtml(String(err))}

`; }); } function renderBottleneckAlert(): void { const container = document.getElementById("bottleneck-alert-container"); if (!container) return; api .fetchBottlenecks() .then((data) => { const bottlenecks = data.bottlenecks || []; if (bottlenecks.length === 0) { container.innerHTML = ""; return; } let html = ""; for (const b of bottlenecks) { if (b.impact === "low") continue; const msg = escapeHtml(b.stage || "Unknown") + " stage averages " + formatDuration(b.avgDuration) + " (" + escapeHtml(b.impact) + " impact)"; const suggestion = b.suggestion ? `
${escapeHtml(b.suggestion)}
` : ""; html += `
\u26A0${msg}${suggestion}
`; } container.innerHTML = html; }) .catch(() => { container.innerHTML = ""; }); } function renderThroughputTrend(): void { const container = document.getElementById("throughput-trend-container"); if (!container) return; api .fetchThroughputTrend() .then((data) => { const points = data.points || []; if (points.length === 0) { container.innerHTML = '

No throughput data

'; return; } container.innerHTML = renderSVGLineChart( points, "throughput", "#4ade80", 300, 100, ); }) .catch((err) => { container.innerHTML = `

Failed to load: ${escapeHtml(String(err))}

`; }); } function renderCapacityForecast(): void { const container = document.getElementById("capacity-forecast-container"); if (!container) return; api .fetchCapacity() .then((data) => { if (!data.rate && !data.queue_clear_hours) { container.innerHTML = '

No capacity data

'; return; } const rate = data.rate != null ? data.rate.toFixed(1) : "?"; const clearTime = data.queue_clear_hours != null ? data.queue_clear_hours.toFixed(1) : "?"; container.innerHTML = `
At current rate (${rate}/hr), queue will clear in ${clearTime} hours
`; }) .catch((err) => { container.innerHTML = `

Failed to load: ${escapeHtml(String(err))}

`; }); } export const metricsView: View = { init() { fetchMetrics(); }, render(_data: FleetState) { // Metrics use cached data; don't re-fetch on every WS push const cache = store.get("metricsCache"); if (cache) renderMetrics(cache); }, destroy() {}, };