WillHbx's picture
Initial commit: Reorganized project structure
aeaa809
Raw
History Blame Contribute Delete
18.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VN Debug Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js"></script>
<style>
:root {
--bg: #0f1117;
--surface: #1a1d27;
--border: #252836;
--text: #dde1f0;
--muted: #6e7891;
--blue: #4c9be8;
--red: #e05c5c;
--green: #56b87a;
--yellow: #e0a840;
--orange: #e09440;
--purple: #b07de8;
--gray: #4e5568;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
font-size: 13px;
padding: 16px;
min-height: 100vh;
}
/* ── Header ─────────────────────────────────────────────────── */
header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
header h1 {
font-size: 17px;
font-weight: 700;
letter-spacing: -.02em;
}
header h1 span { color: var(--blue); }
.status {
display: flex;
align-items: center;
gap: 7px;
font-size: 12px;
color: var(--muted);
}
.dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--gray);
flex-shrink: 0;
transition: background 0.3s;
}
.dot.live { background: var(--green); box-shadow: 0 0 6px var(--green); }
.dot.error { background: var(--red); }
/* ── KPI cards ──────────────────────────────────────────────── */
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 12px;
}
@media (max-width: 800px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } }
.kpi {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
}
.kpi-label {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .07em;
color: var(--muted);
margin-bottom: 5px;
}
.kpi-value {
font-size: 28px;
font-weight: 800;
line-height: 1;
font-variant-numeric: tabular-nums;
color: var(--text);
}
.kpi-sub {
font-size: 11px;
color: var(--muted);
margin-top: 5px;
}
.c-blue { color: var(--blue) !important; }
.c-green { color: var(--green) !important; }
.c-yellow { color: var(--yellow) !important; }
.c-red { color: var(--red) !important; }
/* ── Charts ──────────────────────────────────────────────────── */
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 12px;
}
@media (max-width: 900px) { .charts-grid { grid-template-columns: 1fr; } }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
}
.card h2 {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .07em;
color: var(--muted);
margin-bottom: 10px;
}
/* ── Operations table ────────────────────────────────────────── */
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
th {
text-align: right;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--muted);
padding: 0 8px 8px;
border-bottom: 1px solid var(--border);
}
th:first-child { text-align: left; }
td {
padding: 6px 8px;
border-bottom: 1px solid var(--border);
text-align: right;
font-variant-numeric: tabular-nums;
color: var(--text);
}
td:first-child { text-align: left; color: var(--muted); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255,255,255,.025); }
.total-row td { font-weight: 700; color: var(--text) !important; }
/* ── Actions ─────────────────────────────────────────────────── */
.actions {
display: flex;
align-items: center;
gap: 10px;
margin-top: 12px;
}
button {
background: var(--blue);
color: #fff;
border: none;
border-radius: 6px;
padding: 7px 14px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
opacity: 1;
transition: opacity .15s;
}
button:hover:not(:disabled) { opacity: .82; }
button:disabled { opacity: .35; cursor: default; }
#report-msg { font-size: 12px; color: var(--green); }
</style>
</head>
<body>
<!-- ── Header ─────────────────────────────────────────────────────── -->
<header>
<h1>VN <span>Debug</span> Dashboard</h1>
<div class="status">
<div class="dot" id="dot"></div>
<span id="status-text">Connecting…</span>
</div>
</header>
<!-- ── KPI cards ──────────────────────────────────────────────────── -->
<div class="kpi-grid">
<div class="kpi">
<div class="kpi-label">Turns</div>
<div class="kpi-value c-blue" id="kpi-turns">β€”</div>
<div class="kpi-sub">completed</div>
</div>
<div class="kpi">
<div class="kpi-label">LLM avg</div>
<div class="kpi-value" id="kpi-llm">β€”</div>
<div class="kpi-sub" id="kpi-llm-sub">p95 β€”</div>
</div>
<div class="kpi">
<div class="kpi-label">Image cache</div>
<div class="kpi-value" id="kpi-cache">β€”</div>
<div class="kpi-sub" id="kpi-cache-sub">hits / total</div>
</div>
<div class="kpi">
<div class="kpi-label">Session</div>
<div class="kpi-value c-blue" id="kpi-elapsed">β€”</div>
<div class="kpi-sub" id="kpi-gpu">β€”</div>
</div>
</div>
<!-- ── Charts ─────────────────────────────────────────────────────── -->
<div class="charts-grid">
<div class="card">
<h2>System Resources β€” last 2 min</h2>
<canvas id="res-chart" height="170"></canvas>
</div>
<div class="card">
<h2>Latency per Turn (ms, stacked)</h2>
<canvas id="lat-chart" height="170"></canvas>
</div>
</div>
<!-- ── Operations table ───────────────────────────────────────────── -->
<div class="card">
<h2>Operations</h2>
<table>
<thead>
<tr>
<th>Operation</th>
<th>n</th>
<th>avg ms</th>
<th>p95 ms</th>
<th>max ms</th>
</tr>
</thead>
<tbody id="ops-body"></tbody>
</table>
</div>
<!-- ── Actions ────────────────────────────────────────────────────── -->
<div class="actions">
<button id="save-btn" onclick="saveReport()">Save report now</button>
<span id="report-msg"></span>
</div>
<script>
// ── Chart defaults ───────────────────────────────────────────────────────
Chart.defaults.color = '#6e7891';
Chart.defaults.borderColor = '#252836';
Chart.defaults.font.size = 10;
const GRID_COLOR = 'rgba(255,255,255,.04)';
const BASE_SCALES = {
x: { ticks: { color: '#555', maxTicksLimit: 8 }, grid: { color: GRID_COLOR } },
y: { ticks: { color: '#555' }, grid: { color: GRID_COLOR } },
};
// ── Resource chart (line) ────────────────────────────────────────────────
const resCtx = document.getElementById('res-chart').getContext('2d');
const resChart = new Chart(resCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'RAM %', yAxisID: 'yPct', data: [],
borderColor: '#4c9be8', backgroundColor: 'rgba(76,155,232,.06)',
borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3,
},
{
label: 'CPU %', yAxisID: 'yPct', data: [],
borderColor: '#e05c5c', backgroundColor: 'rgba(224,92,92,.05)',
borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3,
},
{
label: 'VRAM', yAxisID: 'yVram', data: [],
borderColor: '#e09440', backgroundColor: 'rgba(224,148,64,.05)',
borderWidth: 1.5, pointRadius: 0, fill: false, tension: 0.3,
},
]
},
options: {
animation: false,
responsive: true,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { labels: { color: '#888', boxWidth: 10, font: { size: 10 } } },
},
scales: {
x: BASE_SCALES.x,
yPct: {
type: 'linear', position: 'left', min: 0, suggestedMax: 100,
ticks: { color: '#555', callback: v => v + '%' },
grid: { color: GRID_COLOR },
},
yVram: {
type: 'linear', position: 'right', min: 0,
ticks: { color: '#7a6040' },
grid: { drawOnChartArea: false },
display: false, // shown only when VRAM data present
},
},
},
});
// ── Latency stacked bar ──────────────────────────────────────────────────
const latCtx = document.getElementById('lat-chart').getContext('2d');
const latChart = new Chart(latCtx, {
type: 'bar',
data: {
labels: [],
datasets: [
{ label: 'LLM', data: [], backgroundColor: '#4c9be8', stack: 's' },
{ label: 'Painter', data: [], backgroundColor: '#56b87a', stack: 's' },
{ label: 'STT', data: [], backgroundColor: '#b07de8', stack: 's' },
{ label: 'Compact', data: [], backgroundColor: '#e09440', stack: 's' },
{ label: 'Other', data: [], backgroundColor: '#4e5568', stack: 's' },
]
},
options: {
animation: false,
responsive: true,
plugins: {
legend: { labels: { color: '#888', boxWidth: 10, font: { size: 10 } } },
tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${Math.round(ctx.raw)} ms` } },
},
scales: {
x: { ...BASE_SCALES.x, stacked: true },
y: {
...BASE_SCALES.y, stacked: true,
title: { display: true, text: 'ms', color: '#555', font: { size: 10 } },
},
},
},
});
// ── Helpers ──────────────────────────────────────────────────────────────
function fmtMs(ms) {
if (ms == null) return 'β€”';
return ms >= 1000 ? (ms / 1000).toFixed(1) + 's' : Math.round(ms) + 'ms';
}
function fmtElapsed(s) {
if (s == null) return 'β€”';
const m = Math.floor(s / 60), sec = Math.floor(s % 60);
return m > 0 ? `${m}m ${sec}s` : `${sec}s`;
}
function fmtTime(ts) {
const d = new Date(ts * 1000);
return d.getMinutes().toString().padStart(2, '0') + ':' + d.getSeconds().toString().padStart(2, '0');
}
function latencyClass(ms) {
if (ms == null) return '';
if (ms < 2000) return 'c-green';
if (ms < 6000) return 'c-yellow';
return 'c-red';
}
function cacheClass(rate) {
if (rate == null) return '';
if (rate >= 0.7) return 'c-green';
if (rate >= 0.4) return 'c-yellow';
return 'c-red';
}
const GPU_LABELS = { nvml: 'NVIDIA GPU', rocm: 'AMD ROCm', mps: 'Apple MPS (unified)' };
const OP_ORDER = [
'total_turn', 'llm_direct', 'compact_memory', 'context_assembly',
'apply_directives', 'save_memory', 'painter_backdrop', 'painter_sprite', 'stt',
];
const OP_NAMES = {
total_turn: 'Turn total',
llm_direct: 'LLM direct_turn',
compact_memory: 'Compact memory (LLM)',
context_assembly: 'Context assembly',
apply_directives: 'apply_directives',
save_memory: 'save_memory',
painter_backdrop: 'Painter β€” backdrop',
painter_sprite: 'Painter β€” sprite',
stt: 'STT transcribe',
};
// ── Main update ──────────────────────────────────────────────────────────
function update(data) {
// KPI: turns
document.getElementById('kpi-turns').textContent = data.turns_completed ?? 'β€”';
// KPI: LLM
const llm = data.ops?.llm_direct;
const kpiLlm = document.getElementById('kpi-llm');
kpiLlm.textContent = fmtMs(llm?.avg_ms);
kpiLlm.className = 'kpi-value ' + latencyClass(llm?.avg_ms);
document.getElementById('kpi-llm-sub').textContent =
llm ? `p95 ${fmtMs(llm.p95_ms)} Β· max ${fmtMs(llm.max_ms)}` : 'no data yet';
// KPI: cache
const rate = data.cache_hit_rate;
const kpiCache = document.getElementById('kpi-cache');
kpiCache.textContent = rate != null ? Math.round(rate * 100) + '%' : 'β€”';
kpiCache.className = 'kpi-value ' + cacheClass(rate);
const tot = (data.cache_hits ?? 0) + (data.cache_misses ?? 0);
document.getElementById('kpi-cache-sub').textContent =
tot > 0 ? `${data.cache_hits} hits / ${tot} total` : 'no renders yet';
// KPI: elapsed + GPU backend
document.getElementById('kpi-elapsed').textContent = fmtElapsed(data.session_elapsed_s);
document.getElementById('kpi-gpu').textContent =
data.gpu_backend ? (GPU_LABELS[data.gpu_backend] ?? data.gpu_backend) : 'No GPU detected';
// Resource chart
const samples = data.samples ?? [];
resChart.data.labels = samples.map(s => fmtTime(s.ts));
resChart.data.datasets[0].data = samples.map(s => s.ram_pct ?? null);
resChart.data.datasets[1].data = samples.map(s => s.cpu_pct ?? null);
const hasVram = samples.some(s => s.vram_mb != null);
if (hasVram) {
// For nvml/rocm: use vram_pct on left axis if available.
// For MPS (unified): express process RSS as % of total system RAM (ram_mb / ram_pct * 100).
resChart.data.datasets[2].data = samples.map(s => {
if (s.vram_mb == null) return null;
if (s.vram_pct != null) return s.vram_pct; // nvml / rocm
// MPS: derive total RAM from ram_mb + ram_pct, express RSS as %
if (s.ram_pct > 0) return (s.vram_mb / (s.ram_mb / (s.ram_pct / 100))) * 100;
return null;
});
const vramLabel = data.gpu_backend === 'mps' ? 'RSS % (MPS)' : 'VRAM %';
resChart.data.datasets[2].label = vramLabel;
resChart.options.scales.yVram.display = false; // keep right axis hidden, same % scale
} else {
resChart.data.datasets[2].data = samples.map(() => null);
}
resChart.update('none');
// Latency stacked bar
const turns = data.turn_breakdown ?? [];
latChart.data.labels = turns.map(t => `T${t.turn}`);
latChart.data.datasets[0].data = turns.map(t => t.llm_direct ?? 0);
latChart.data.datasets[1].data = turns.map(t => (t.painter_backdrop ?? 0) + (t.painter_sprite ?? 0));
latChart.data.datasets[2].data = turns.map(t => t.stt ?? 0);
latChart.data.datasets[3].data = turns.map(t => t.compact_memory ?? 0);
latChart.data.datasets[4].data = turns.map(t => {
const acc = (t.llm_direct ?? 0) + (t.painter_backdrop ?? 0) + (t.painter_sprite ?? 0)
+ (t.stt ?? 0) + (t.compact_memory ?? 0)
+ (t.apply_directives ?? 0) + (t.save_memory ?? 0);
return Math.max(0, (t.total_turn ?? 0) - acc);
});
latChart.update('none');
// Ops table
const ops = data.ops ?? {};
const keys = [...OP_ORDER.filter(k => k in ops), ...Object.keys(ops).filter(k => !OP_ORDER.includes(k))];
const tbody = document.getElementById('ops-body');
tbody.innerHTML = keys.map(k => {
const o = ops[k];
if (!o) return '';
const isTot = k === 'total_turn';
return `<tr class="${isTot ? 'total-row' : ''}">
<td>${OP_NAMES[k] ?? k}</td>
<td>${o.count}</td>
<td class="${latencyClass(o.avg_ms)}">${Math.round(o.avg_ms)}</td>
<td>${Math.round(o.p95_ms)}</td>
<td>${Math.round(o.max_ms)}</td>
</tr>`;
}).join('');
}
// ── SSE connection with auto-reconnect ───────────────────────────────────
const dot = document.getElementById('dot');
const statusTxt = document.getElementById('status-text');
let src = null;
function connect() {
if (src) { src.close(); src = null; }
dot.className = 'dot';
statusTxt.textContent = 'Connecting…';
src = new EventSource('/debug/stream');
src.onopen = () => {
dot.className = 'dot live';
statusTxt.textContent = 'Live';
};
src.onmessage = e => {
try { update(JSON.parse(e.data)); } catch { /* ignore parse errors */ }
};
src.onerror = () => {
dot.className = 'dot error';
statusTxt.textContent = 'Reconnecting in 3s…';
src.close(); src = null;
setTimeout(connect, 3000);
};
}
// ── Save report ──────────────────────────────────────────────────────────
async function saveReport() {
const btn = document.getElementById('save-btn');
const msg = document.getElementById('report-msg');
btn.disabled = true;
msg.style.color = 'var(--muted)';
msg.textContent = 'Saving…';
try {
const r = await fetch('/debug/report');
const d = await r.json();
msg.style.color = 'var(--green)';
msg.textContent = d.status === 'ok' ? 'βœ“ Saved to runs/debug_*/' : 'βœ— Save failed';
} catch {
msg.style.color = 'var(--red)';
msg.textContent = 'βœ— Request failed';
}
setTimeout(() => { msg.textContent = ''; btn.disabled = false; }, 4000);
}
connect();
</script>
</body>
</html>