akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
730 lines (665 loc) • 35.2 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%%REPORT_TITLE%%</title>
<!-- %%ECHARTS_TAG%% : a <script src> pointed at the jsDelivr CDN (chunk-9
WI-9.4d) — viewing this report requires network access. -->
%%ECHARTS_TAG%%
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d1117;
--surface: #161b22;
--surface2:#21262d;
--border: #30363d;
--text: #e6edf3;
--muted: #9ea8b5;
--accent: #58a6ff;
--green: #3fb950;
--yellow: #d29922;
--red: #f85149;
--purple: #bc8cff;
--neutral: #8b949e;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', sans-serif;
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.6;
}
/* ── Header ─────────────────────────────────────────────────────────── */
header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 16px 20px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
header .logo { font-size: 20px; font-weight: 700; color: var(--accent); letter-spacing: -0.5px; flex-shrink: 0; display: flex; flex-direction: column; line-height: 1.1; }
header .logo-version { font-size: 10px; font-weight: 500; color: var(--muted); letter-spacing: 0; }
header .meta { color: var(--muted); font-size: 13px; min-width: 0; }
header .meta .title { font-weight: 600; color: var(--text); }
header .badges { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
@media (max-width: 600px) {
header { padding: 12px 16px; gap: 8px; }
header .badges { margin-left: 0; width: 100%; }
}
/* ── Badges ─────────────────────────────────────────────────────────── */
.badge-pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;
}
.badge-warn { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid rgba(210,153,34,0.3); }
.badge-pass { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid rgba(63,185,80,0.3); }
.badge-fail { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid rgba(248,81,73,0.3); }
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.dot-warn { background: var(--yellow); }
.dot-pass { background: var(--green); }
.dot-fail { background: var(--red); }
/* ── Layout ─────────────────────────────────────────────────────────── */
main { padding: 20px 32px; max-width: 1400px; margin: 0 auto; }
@media (max-width: 768px) { main { padding: 16px; } }
.section { margin-bottom: 32px; }
h2 { font-size: 16px; font-weight: 600; margin-bottom: 16px; }
h3 { font-size: 13px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
abbr[title] { text-decoration: underline dotted; cursor: help; text-underline-offset: 2px; }
/* ── Executive summary (Discord card) ───────────────────────────────── */
.exec {
background: linear-gradient(180deg, var(--surface) 0%, #12161c 100%);
border: 1px solid var(--border); border-left: 4px solid var(--accent);
border-radius: 10px; padding: 18px 22px; margin-bottom: 28px;
}
.exec h2 { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.exec .verdict {
font-size: 14px; color: var(--text); margin-bottom: 16px;
padding: 10px 14px; background: var(--surface2); border-radius: 8px;
border-left: 3px solid var(--accent);
}
.exec .verdict b { font-weight: 700; }
.exec .verdict.pass { border-left-color: var(--green); }
.exec .verdict.warn { border-left-color: var(--yellow); }
.exec .verdict.fail { border-left-color: var(--red); }
.exec .freshness { font-size: 12px; color: var(--muted); margin-bottom: 14px; }
.exec .freshness.stale { color: var(--yellow); font-weight: 600; }
.exec .exec-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
@media (max-width: 900px) { .exec .exec-grid { grid-template-columns: 1fr; gap: 16px; } }
.exec . blk-title, .exec h4 {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px;
color: var(--muted); margin-bottom: 8px; font-weight: 600;
}
.exec ul { list-style: none; }
.exec li { display: flex; justify-content: space-between; gap: 12px; padding: 3px 0; font-size: 13px; border-bottom: 1px dashed rgba(48,54,61,0.6); }
.exec li:last-child { border-bottom: none; }
.exec li .k { color: var(--muted); }
.exec li .v { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; text-align: right; }
.trend-pill {
display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px;
border-radius: 12px; font-size: 11px; font-weight: 600;
}
.trend-pill.up { background: rgba(63,185,80,0.15); color: var(--green); }
.trend-pill.down { background: rgba(248,81,73,0.15); color: var(--red); }
.trend-pill.flat { background: rgba(139,148,158,0.12);color: var(--muted); }
.exec .overall {
margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border);
font-size: 13px; color: var(--muted);
}
.exec .overall b { color: var(--text); }
/* ── KPI cards ──────────────────────────────────────────────────────── */
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 10px; margin-bottom: 28px;
}
.filter-bar {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; padding: 12px 14px; margin-bottom: 20px;
}
.filter-bar label { color: var(--muted); font-size: 12px; font-weight: 600; }
.filter-bar select {
background: var(--surface2); color: var(--text); border: 1px solid var(--border);
border-radius: 6px; padding: 6px 10px; font-size: 12px;
}
.filter-note { color: var(--muted); font-size: 12px; margin-left: auto; }
@media (max-width: 600px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 20px; } }
.kpi-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; }
.kpi-card .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
.kpi-card .value { font-size: 26px; font-weight: 700; line-height: 1; color: var(--text); }
.kpi-card .sub { font-size: 11px; color: var(--muted); margin-top: 4px; }
@media (max-width: 600px) { .kpi-card .value { font-size: 22px; } }
.kpi-card.green .value { color: var(--green); }
.kpi-card.yellow .value { color: var(--yellow); }
.kpi-card.red .value { color: var(--red); }
.kpi-card.blue .value { color: var(--accent); }
.kpi-card.purple .value { color: var(--purple); }
.kpi-card.neutral .value { color: var(--text); }
.kpi-card.green { border-left: 3px solid var(--green); }
.kpi-card.yellow { border-left: 3px solid var(--yellow); }
.kpi-card.red { border-left: 3px solid var(--red); }
/* ── Chart panels ───────────────────────────────────────────────────── */
.charts-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px; margin-bottom: 28px;
}
@media (max-width: 768px) {
.charts-grid { grid-template-columns: 1fr; gap: 10px; }
.chart-panel.full-width { grid-column: auto; }
}
.chart-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; position: relative; }
.chart-panel.full-width { grid-column: 1 / -1; }
.chart-panel .ec { width: 100%; height: 240px; }
.chart-panel.full-width .ec { height: 280px; }
.chart-panel .ec.tall { height: 300px; }
.chart-panel.full-width .ec.tall { height: 320px; }
@media (max-width: 600px) { .chart-panel .ec { height: 200px; } }
.empty-overlay {
position: absolute; inset: 44px 16px 16px 16px;
display: none; align-items: center; justify-content: center;
color: var(--muted); font-size: 13px; text-align: center;
pointer-events: none;
}
.empty-overlay.show { display: flex; }
/* ── Tables ─────────────────────────────────────────────────────────── */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 480px; }
thead th {
text-align: left; padding: 8px 12px;
font-size: 11px; font-weight: 600; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.4px;
border-bottom: 1px solid var(--border); white-space: nowrap;
}
tbody tr { border-bottom: 1px solid var(--border); }
tbody tr:hover { background: var(--surface2); }
tbody td { padding: 9px 12px; color: var(--text); }
@media (max-width: 600px) {
table { font-size: 12px; }
thead th, tbody td { padding: 7px 10px; }
}
@media (hover: none) { tbody tr:active { background: var(--surface2); } }
/* ── Tags ───────────────────────────────────────────────────────────── */
.tag { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; }
.tag-extract { background: rgba(88,166,255,0.15); color: var(--accent); }
.tag-consolidate { background: rgba(188,140,255,0.15); color: var(--purple); }
.prio { display: inline-flex; align-items: center; padding: 1px 7px; border-radius: 4px; font-size: 11px; font-weight: 700; margin-right: 6px; }
.prio.p1 { background: rgba(248,81,73,0.18); color: var(--red); }
.prio.p2 { background: rgba(210,153,34,0.18); color: var(--yellow); }
.prio.p3 { background: rgba(139,148,158,0.15);color: var(--muted); }
/* ── Advisories / Action items ──────────────────────────────────────── */
.advisory-list { display: flex; flex-direction: column; gap: 10px; }
.advisory {
display: flex; gap: 12px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; padding: 14px 16px;
}
.advisory.warn { border-left: 3px solid var(--yellow); }
.advisory.fail { border-left: 3px solid var(--red); }
.advisory-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
.advisory-body .title { font-weight: 600; font-size: 13px; margin-bottom: 4px; }
.advisory-body .desc { font-size: 12px; color: var(--muted); word-break: break-word; }
.advisory-body .remedy { font-size: 12px; margin-top: 6px; }
/* ── Trend indicators ───────────────────────────────────────────────── */
.trend { font-size: 12px; }
.trend-up { color: var(--green); }
.trend-down { color: var(--red); }
.trend-flat { color: var(--muted); }
/* ── Two-col ─────────────────────────────────────────────────────────── */
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } }
/* ── Summary table wrapper ──────────────────────────────────────────── */
.summary-table { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
/* ── Code / command block ───────────────────────────────────────────── */
code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
font-size: 11px; background: var(--surface2);
padding: 2px 6px; border-radius: 4px; color: var(--accent); word-break: break-all;
}
/* ── Footer ─────────────────────────────────────────────────────────── */
footer {
border-top: 1px solid var(--border); padding: 14px 32px;
color: var(--muted); font-size: 12px;
display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px;
}
@media (max-width: 600px) { footer { padding: 12px 16px; flex-direction: column; } }
</style>
</head>
<body>
<header>
<div class="logo">AKM<span class="logo-version">v%%AKM_VERSION%%</span></div>
<div class="meta">
<div class="title">Improve Health Report</div>
<div>Window: %%WINDOW%% · %%SINCE_HUMAN%% · %%RUN_COUNT%% runs</div>
</div>
<div class="badges">
%%STATUS_BADGE_HTML%%
</div>
</header>
<main>
<!-- Executive Summary (HTML rendering of the Discord/improve-health text report) -->
<section class="exec">
%%EXEC_SUMMARY_HTML%%
</section>
<!-- Action Items (merged + de-duplicated advisories and what-to-watch) -->
<div class="section">
<h2>Action Items</h2>
<div class="advisory-list">
%%ACTION_ITEMS_HTML%%
</div>
</div>
<!-- KPI Row -->
<div class="kpi-grid">
%%KPI_CARDS_HTML%%
</div>
<!-- Summary -->
<div class="section">
<h2>%%WINDOW%% Aggregate Summary</h2>
<div class="summary-table">
<div class="table-wrap">
<table>
<thead><tr><th>Metric</th><th>Value</th><th>Trend</th></tr></thead>
<tbody>
%%SUMMARY_ROWS_HTML%%
</tbody>
</table>
</div>
</div>
</div>
<div class="filter-bar">
<label for="rangeSelect">Slice</label>
<select id="rangeSelect">
%%SLICE_OPTIONS_HTML%%
</select>
<label for="taskFilter">Task</label>
<select id="taskFilter"></select>
<label for="statusFilter">Status</label>
<select id="statusFilter">
<option value="all" selected>all</option>
<option value="ok">ok</option>
<option value="failed">failed</option>
</select>
<div class="filter-note">Filters affect charts and the Recent Runs table.</div>
</div>
<!-- Charts -->
<div class="section">
<h2>Run Trend Lines (%%WINDOW%%)</h2>
<div class="charts-grid">
<div class="chart-panel full-width">
<h3>Wall Time — successful vs failed runs (min)</h3>
<div id="chartWallTime" class="ec tall" role="img" aria-label="Line chart of wall time per run in minutes with a rolling average overlay"></div>
<div class="empty-overlay" id="emptyWallTime">No runs in the selected slice.</div>
</div>
<div class="chart-panel full-width">
<h3>Per-Phase Wall Time Decomposition (stacked, min)</h3>
<div id="chartPhases" class="ec tall" role="img" aria-label="Stacked area chart decomposing wall time per run into consolidation, memory inference, and unattributed phases"></div>
<div class="empty-overlay" id="emptyPhases">No runs in the selected slice.</div>
</div>
<div class="chart-panel full-width">
<h3>LLM Tokens per Stage (stacked: prompt / completion / reasoning)</h3>
<div id="chartLlmStages" class="ec" role="img" aria-label="Horizontal stacked bar chart of LLM token usage per pipeline stage, broken into prompt, completion, and reasoning tokens"></div>
<div class="empty-overlay" id="emptyLlmStages">No per-stage LLM usage recorded in this window.</div>
</div>
<div class="chart-panel">
<h3>Stash Growth (derived vs eligible)</h3>
<div id="chartStash" class="ec tall" role="img" aria-label="Line chart of stash derived versus eligible asset counts per run"></div>
<div class="empty-overlay" id="emptyStash">No runs in the selected slice.</div>
</div>
<div class="chart-panel">
<h3>Consolidation Output (promoted line · merged / deleted bars)</h3>
<div id="chartConsOutput" class="ec tall" role="img" aria-label="Consolidation output per run: promoted as a line on the right axis, merged and deleted as bars on the left axis"></div>
<div class="empty-overlay" id="emptyConsOutput">No runs in the selected slice.</div>
</div>
<div class="chart-panel">
<h3>Success / Failures per Run</h3>
<div id="chartSuccess" class="ec" role="img" aria-label="Line chart of wall time for successful runs with failed runs marked as red triangles"></div>
<div class="empty-overlay" id="emptySuccess">No runs in the selected slice.</div>
</div>
<div class="chart-panel">
<h3>Lint Flagged vs Fixed</h3>
<div id="chartLint" class="ec" role="img" aria-label="Line chart of lint issues flagged versus fixed per run"></div>
<div class="empty-overlay" id="emptyLint">No runs in the selected slice.</div>
</div>
<div class="chart-panel full-width">
<h3>Distill Skip-Reason Breakdown (stacked)</h3>
<div id="chartDistill" class="ec tall" role="img" aria-label="Stacked bar chart of distill skip reasons per run"></div>
<div class="empty-overlay" id="emptyDistill">No distill skips recorded in this window.</div>
</div>
</div>
</div>
<!-- Proposals -->
<div class="section">
<h2>Pending Proposals (%%PROPOSAL_COUNT%%)</h2>
<div class="summary-table">
<div class="table-wrap">
<table>
<thead><tr><th>#</th><th>Ref</th><th>Source</th><th>Created</th></tr></thead>
<tbody>
%%PROPOSAL_ROWS_HTML%%
</tbody>
</table>
</div>
</div>
</div>
<!-- Recent runs -->
<div class="section">
<h2>Recent Runs</h2>
<div class="summary-table">
<div class="table-wrap">
<table>
<thead>
<tr><th>Started</th><th>Task</th><th>Strategy</th><th>Wall</th><th>Promoted</th><th>Merged</th><th>Contradicted</th><th>MI Written</th><th>Lint Fixed</th><th>Status</th></tr>
</thead>
<tbody id="lastRunsTable"></tbody>
</table>
</div>
</div>
</div>
</main>
<footer>
<span>AKM v%%AKM_VERSION%% · Window: %%WINDOW%% · %%REPORT_TITLE%%</span>
<span>Generated <time data-iso="%%GENERATED_AT%%">%%GENERATED_AT%%</time></span>
</footer>
<script>
// ── Injected run data ─────────────────────────────────────────────────────────
%%RUNS_JS_CONST%%
const DISTILL_REASONS = %%DISTILL_REASONS_JSON%%;
const LLM_BY_STAGE = %%LLM_BY_STAGE_JSON%%;
// ── Theme constants ───────────────────────────────────────────────────────────
const C = {
bg: '#161b22', surface2: '#21262d', border: '#30363d',
text: '#e6edf3', muted: '#9ea8b5',
accent: '#58a6ff', green: '#3fb950', yellow: '#d29922',
red: '#f85149', purple: '#bc8cff',
};
const SERIES_PALETTE = [C.accent, C.green, C.yellow, C.purple, C.red, '#39c5cf', '#db61a2', '#e3b341'];
const toMin = ms => ms ? +(ms / 60000).toFixed(2) : 0;
function makeXLabels(rs) {
return rs.map(r => {
const d = new Date(r.startedAt);
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
const hh = String(d.getHours()).padStart(2, '0');
const mi = String(d.getMinutes()).padStart(2, '0');
return `${mm}-${dd} ${hh}:${mi}`;
});
}
function latestRunTime(rs) {
if (!rs.length) return 0;
return Math.max(...rs.map(r => Date.parse(r.completedAt || r.startedAt || 0)).filter(Number.isFinite));
}
function filteredRuns() {
const slice = document.getElementById('rangeSelect')?.value || 'all';
const task = document.getElementById('taskFilter')?.value || 'all';
const status = document.getElementById('statusFilter')?.value || 'all';
// slice value is 'all' (full window) or a cutoff in ms relative to the newest run.
const startMs = slice === 'all' ? -Infinity : latestRunTime(RUNS) - Number(slice);
return RUNS.filter(r => {
const ts = Date.parse(r.completedAt || r.startedAt || 0);
if (Number.isFinite(ts) && ts < startMs) return false;
if (task !== 'all' && (r.taskId || 'manual') !== task) return false;
if (status === 'ok' && !r.ok) return false;
if (status === 'failed' && r.ok) return false;
return true;
});
}
function populateFilters() {
const taskSelect = document.getElementById('taskFilter');
const tasks = ['all', ...Array.from(new Set(RUNS.map(r => r.taskId || 'manual'))).sort()];
taskSelect.innerHTML = tasks.map(t => `<option value="${t}">${t}</option>`).join('');
}
function toggleEmpty(id, isEmpty) {
const el = document.getElementById(id);
if (el) el.classList.toggle('show', !!isEmpty);
}
// Centered rolling average (window = ~10% of runs, min 3) — deterministic.
function rollingAvg(arr) {
const n = arr.length;
const w = Math.max(3, Math.floor(n / 10));
const half = Math.floor(w / 2);
return arr.map((_, i) => {
let sum = 0, cnt = 0;
for (let j = i - half; j <= i + half; j++) {
if (j >= 0 && j < n) { sum += arr[j]; cnt++; }
}
return cnt ? +(sum / cnt).toFixed(2) : 0;
});
}
const baseAxis = {
axisLine: { lineStyle: { color: C.border } },
axisLabel: { color: C.muted, fontSize: 10 },
splitLine: { lineStyle: { color: 'rgba(48,54,61,0.5)' } },
};
const baseGrid = { left: 48, right: 16, top: 28, bottom: 28, containLabel: true };
const denseGrid = { left: 48, right: 16, top: 28, bottom: 40, containLabel: true };
const baseTooltip = {
trigger: 'axis',
backgroundColor: C.surface2, borderColor: C.border, borderWidth: 1,
textStyle: { color: C.text, fontSize: 12 },
};
const baseLegend = { textStyle: { color: C.muted, fontSize: 11 }, top: 2, type: 'scroll' };
const denseZoom = [{ type: 'slider', bottom: 4, height: 18, borderColor: C.border, backgroundColor: 'rgba(33,38,45,0.6)', fillerColor: 'rgba(88,166,255,0.18)', handleStyle: { color: C.accent }, textStyle: { color: C.muted, fontSize: 9 } }];
function mkChart(id, option) {
const el = document.getElementById(id);
if (!el) return;
const prior = echarts.getInstanceByDom(el);
if (prior) prior.dispose();
const chart = echarts.init(el, null, { renderer: 'canvas' });
// animation off → deterministic first paint & no per-frame randomness
option.animation = false;
option.textStyle = { color: C.text };
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
return chart;
}
// ── 1. Wall time per run + rolling avg ────────────────────────────────────────
function renderCharts(rs) {
const xLabels = makeXLabels(rs);
toggleEmpty('emptyWallTime', !rs.length);
toggleEmpty('emptyPhases', !rs.length);
toggleEmpty('emptyStash', !rs.length);
toggleEmpty('emptyConsOutput', !rs.length);
toggleEmpty('emptySuccess', !rs.length);
toggleEmpty('emptyLint', !rs.length);
const wall = rs.map(r => toMin(r.wallTimeMs));
const failPoints = rs.map((r, i) => r.ok ? null : [i, toMin(r.wallTimeMs)]).filter(Boolean);
mkChart('chartWallTime', {
tooltip: baseTooltip,
legend: { ...baseLegend, data: ['Wall time', 'Rolling avg', 'Failed run'] },
grid: denseGrid,
dataZoom: denseZoom,
xAxis: { type: 'category', data: xLabels, ...baseAxis },
yAxis: { type: 'value', name: 'min', nameTextStyle: { color: C.muted }, ...baseAxis },
series: [
{ name: 'Wall time', type: 'line', data: wall, showSymbol: false, smooth: true,
lineStyle: { color: C.accent, width: 1.5 }, areaStyle: { color: 'rgba(88,166,255,0.08)' } },
{ name: 'Rolling avg', type: 'line', data: rollingAvg(wall), showSymbol: false, smooth: true,
lineStyle: { color: C.yellow, width: 2, type: 'dashed' } },
{ name: 'Failed run', type: 'scatter', symbol: 'triangle', symbolSize: 12,
itemStyle: { color: C.red }, data: failPoints },
],
});
// ── 2. Per-phase wall time decomposition (stacked area) ──────────────────────
mkChart('chartPhases', {
tooltip: baseTooltip,
legend: { ...baseLegend, data: ['Consolidation', 'Memory inference', 'Unattributed'] },
grid: denseGrid,
dataZoom: denseZoom,
xAxis: { type: 'category', data: xLabels, ...baseAxis },
yAxis: { type: 'value', name: 'min', nameTextStyle: { color: C.muted }, ...baseAxis },
series: [
{ name: 'Consolidation', type: 'line', stack: 'p', areaStyle: { color: 'rgba(88,166,255,0.55)' }, lineStyle: { width: 0 }, showSymbol: false, data: rs.map(r => toMin(r.consDurationMs)) },
{ name: 'Memory inference', type: 'line', stack: 'p', areaStyle: { color: 'rgba(63,185,80,0.55)' }, lineStyle: { width: 0 }, showSymbol: false, data: rs.map(r => toMin(r.miDurationMs)) },
{ name: 'Unattributed', type: 'line', stack: 'p', areaStyle: { color: 'rgba(210,153,34,0.45)' }, lineStyle: { width: 0 }, showSymbol: false, data: rs.map(r => toMin(r.otherMs)) },
],
});
// ── 3. Stash growth (dual line) ───────────────────────────────────────────────
mkChart('chartStash', {
tooltip: baseTooltip,
legend: { ...baseLegend, data: ['Derived', 'Eligible'] },
grid: denseGrid,
dataZoom: denseZoom,
xAxis: { type: 'category', data: xLabels, ...baseAxis },
yAxis: { type: 'value', ...baseAxis },
series: [
{ name: 'Derived', type: 'line', showSymbol: false, smooth: true, lineStyle: { color: C.green, width: 1.8 }, data: rs.map(r => r.derived) },
{ name: 'Eligible', type: 'line', showSymbol: false, smooth: true, lineStyle: { color: C.accent, width: 1.8 }, data: rs.map(r => r.eligible) },
],
});
// ── 4. Consolidation output (promoted / merged / deleted) ────────────────────
mkChart('chartConsOutput', {
tooltip: baseTooltip,
legend: { ...baseLegend, data: ['Promoted', 'Merged', 'Deleted'] },
grid: denseGrid,
dataZoom: denseZoom,
xAxis: { type: 'category', data: xLabels, ...baseAxis },
// Promoted dwarfs merged/deleted, so it gets its own right-hand axis and is
// drawn as a line; merged/deleted stay as bars on the left axis where their
// smaller magnitudes remain readable.
yAxis: [
{ type: 'value', name: 'merged / deleted', nameTextStyle: { color: C.muted, fontSize: 10 }, ...baseAxis },
{ type: 'value', name: 'promoted', nameTextStyle: { color: C.muted, fontSize: 10 }, position: 'right', ...baseAxis, splitLine: { show: false } },
],
series: [
{ name: 'Promoted', type: 'line', yAxisIndex: 1, showSymbol: false, smooth: true, lineStyle: { color: C.accent, width: 1.8 }, itemStyle: { color: C.accent }, data: rs.map(r => r.promoted) },
{ name: 'Merged', type: 'bar', yAxisIndex: 0, itemStyle: { color: 'rgba(63,185,80,0.75)' }, data: rs.map(r => r.merged) },
{ name: 'Deleted', type: 'bar', yAxisIndex: 0, itemStyle: { color: 'rgba(248,81,73,0.75)' }, data: rs.map(r => r.deleted) },
],
});
// ── 5. Success / failures (failed runs as distinct red triangles) ────────────
const failScatter = rs.map((r, i) => r.ok ? null : [i, toMin(r.wallTimeMs)]).filter(Boolean);
mkChart('chartSuccess', {
tooltip: {
...baseTooltip,
formatter: p => {
const arr = Array.isArray(p) ? p : [p];
const i = arr[0].dataIndex;
return `${xLabels[i]}<br/>${rs[i].ok ? '✅ ok' : '❌ failed'} · ${toMin(rs[i].wallTimeMs)}m`;
},
},
legend: { ...baseLegend, data: ['Wall time (ok)', 'Failed run'] },
grid: baseGrid,
xAxis: { type: 'category', data: xLabels, ...baseAxis },
yAxis: { type: 'value', name: 'min', nameTextStyle: { color: C.muted }, ...baseAxis },
series: [
{ name: 'Wall time (ok)', type: 'line', showSymbol: false, smooth: true,
lineStyle: { color: C.green, width: 1.2 }, data: rs.map(r => r.ok ? toMin(r.wallTimeMs) : null) },
{ name: 'Failed run', type: 'scatter', symbol: 'triangle', symbolSize: 12,
itemStyle: { color: C.red },
data: failScatter },
],
});
// ── 6. Lint flagged vs fixed ──────────────────────────────────────────────────
mkChart('chartLint', {
tooltip: baseTooltip,
legend: { ...baseLegend, data: ['Flagged', 'Fixed'] },
grid: baseGrid,
xAxis: { type: 'category', data: xLabels, ...baseAxis },
yAxis: { type: 'value', ...baseAxis },
series: [
{ name: 'Flagged', type: 'line', showSymbol: false, smooth: true, lineStyle: { color: C.yellow, width: 1.6 }, areaStyle: { color: 'rgba(210,153,34,0.10)' }, data: rs.map(r => r.lintFlagged) },
{ name: 'Fixed', type: 'line', showSymbol: false, smooth: true, lineStyle: { color: C.green, width: 1.6 }, data: rs.map(r => r.lintFixed) },
],
});
// ── 7. Distill skip-reason breakdown (stacked bar) ───────────────────────────
const distillSeries = DISTILL_REASONS.map((reason, idx) => ({
name: reason,
type: 'bar',
stack: 'd',
itemStyle: { color: SERIES_PALETTE[idx % SERIES_PALETTE.length] },
data: rs.map(r => (r.distillByReason && r.distillByReason[reason]) || 0),
}));
const distillEmpty = !rs.length || !DISTILL_REASONS.length;
toggleEmpty('emptyDistill', distillEmpty);
mkChart('chartDistill', {
tooltip: baseTooltip,
legend: { ...baseLegend, data: DISTILL_REASONS },
grid: { ...denseGrid, top: 40 },
dataZoom: denseZoom,
xAxis: { type: 'category', data: xLabels, ...baseAxis },
yAxis: { type: 'value', ...baseAxis },
series: distillSeries.length ? distillSeries
: [{ name: 'none', type: 'bar', data: rs.map(() => 0) }],
});
}
// ── 8. LLM tokens per stage (horizontal stacked bar) — window aggregate ──────
function humanizeStage(s) {
return String(s).replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
function renderLlmStages() {
const stages = Object.keys(LLM_BY_STAGE).sort();
toggleEmpty('emptyLlmStages', !stages.length);
const labels = stages.map(humanizeStage);
const prompt = stages.map(s => LLM_BY_STAGE[s].promptTokens || 0);
const completion = stages.map(s => LLM_BY_STAGE[s].completionTokens || 0);
const reasoning = stages.map(s => LLM_BY_STAGE[s].reasoningTokens || 0);
mkChart('chartLlmStages', {
tooltip: { ...baseTooltip, trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { ...baseLegend, data: ['Prompt', 'Completion', 'Reasoning'] },
grid: { left: 8, right: 24, top: 28, bottom: 8, containLabel: true },
xAxis: { type: 'value', ...baseAxis },
yAxis: { type: 'category', data: labels, ...baseAxis },
series: [
{ name: 'Prompt', type: 'bar', stack: 'tok', itemStyle: { color: 'rgba(88,166,255,0.8)' }, data: prompt },
{ name: 'Completion', type: 'bar', stack: 'tok', itemStyle: { color: 'rgba(63,185,80,0.8)' }, data: completion },
{ name: 'Reasoning', type: 'bar', stack: 'tok', itemStyle: { color: 'rgba(188,140,255,0.8)' }, data: reasoning },
],
});
}
// ── Recent runs table ─────────────────────────────────────────────────────────
function renderLastRuns(rs) {
const tbody = document.getElementById('lastRunsTable');
tbody.innerHTML = '';
[...rs].reverse().forEach(r => {
const tr = document.createElement('tr');
const d = new Date(r.startedAt);
const pad = n => String(n).padStart(2, '0');
const ts = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
const dur = r.wallTimeMs ? (r.wallTimeMs / 60000).toFixed(1) + 'm' : '—';
const task = r.taskId || 'manual';
const strategy = r.strategy || '—';
const badge = r.ok
? '<span class="badge-pill badge-pass" style="padding:2px 8px;font-size:11px;"><span class="dot dot-pass" style="width:6px;height:6px;"></span> ok</span>'
: '<span class="badge-pill badge-fail" style="padding:2px 8px;font-size:11px;"><span class="dot dot-fail" style="width:6px;height:6px;"></span> failed</span>';
tr.innerHTML = `
<td style="font-family:monospace;font-size:12px;">${ts}</td>
<td><code>${task}</code></td>
<td><code>${strategy}</code></td>
<td>${dur}</td>
<td style="color:var(--accent);font-weight:600;">${r.promoted || 0}</td>
<td>${r.merged || 0}</td>
<td style="color:${(r.contradicted||0)>5?'var(--yellow)':'var(--text)'};">${r.contradicted || 0}</td>
<td style="color:var(--green);">${r.miWritten || 0}</td>
<td>${r.lintFixed || 0}</td>
<td>${badge}</td>
`;
tbody.appendChild(tr);
});
}
function updateFilteredViews() {
const rs = filteredRuns();
renderCharts(rs);
renderLastRuns(rs);
}
populateFilters();
document.getElementById('rangeSelect').addEventListener('change', updateFilteredViews);
document.getElementById('taskFilter').addEventListener('change', updateFilteredViews);
document.getElementById('statusFilter').addEventListener('change', updateFilteredViews);
renderLlmStages();
updateFilteredViews();
// Reformat server-rendered ISO timestamps to the viewer's local timezone.
(function() {
const pad = n => String(n).padStart(2, '0');
document.querySelectorAll('time[data-iso]').forEach(el => {
const d = new Date(el.dataset.iso);
if (!isNaN(d)) {
el.textContent = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
});
})();
</script>
</body>
</html>