// 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 = `
): void {
const container = document.getElementById("stage-breakdown");
if (!container) return;
const keys = Object.keys(stageDurations);
if (keys.length === 0) {
container.innerHTML = '';
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 = '';
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 +=
'COST BY MODEL
';
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 +=
'COST BY STAGE
';
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 +=
'COST PER ISSUE
';
html +=
'
| Issue | Cost |
';
const sorted = [...data.by_issue].sort(
(a, b) => (b.cost || 0) - (a.cost || 0),
);
sorted.forEach((item) => {
html += `| #${item.issue} | $${(item.cost || 0).toFixed(2)} |
`;
});
html += "
";
}
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 +=
`BUDGET UTILIZATION
` +
`
` +
`
$${data.spent.toFixed(2)} / $${data.budget.toFixed(2)} (${budgetPct.toFixed(0)}%) `;
}
container.innerHTML =
html || '';
})
.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 =
'';
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 =
'| Stage | Avg | Min | Max | Count | Trend |
';
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 +=
`| ${escapeHtml(s.name || s.stage || "")} | ${formatDuration(s.avg_s)} | ${formatDuration(s.min_s)} | ` +
`${formatDuration(s.max_s)} | ${s.count || 0} | ${trendArrow} |
`;
}
html += "
";
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 =
'';
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 =
'';
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() {},
};