UNPKG

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
<!DOCTYPE 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%% &nbsp;·&nbsp; %%SINCE_HUMAN%% &nbsp;·&nbsp; %%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%% &nbsp;·&nbsp; Window: %%WINDOW%% &nbsp;·&nbsp; %%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>&nbsp;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>&nbsp;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>