diff --git a/static/index.html b/static/index.html index fe14ef5..cac7f8d 100644 --- a/static/index.html +++ b/static/index.html @@ -316,13 +316,32 @@ display: flex; align-items: center; /* vertically center the button with the headline */ gap: 1rem; - } + } + .logo-chart-row { + display: flex; + align-items: stretch; + gap: 1rem; + margin-bottom: 1rem; + } + #header-tps-container { + flex: 1; + background: white; + border-radius: 6px; + padding: 0.25rem 0.75rem; + height: 100px; + position: relative; + } - +
+ +
+ +
+

Router Dashboard

@@ -419,6 +438,11 @@ let statsChart = null; let rawTimeSeries = null; let totalTokensChart = null; + let headerTpsChart = null; + const TPS_HISTORY_SIZE = 60; + const tpsHistory = []; + let latestPerModelTokens = {}; + const modelFirstSeen = {}; let usageSource = null; const API_KEY_STORAGE_KEY = "nomyo-router-api-key"; @@ -928,7 +952,7 @@ function renderTimeSeriesChart(timeSeriesData, chart, minutes) { const uniqueEndpoints = Array.from(new Set(endpoints)); const endpointsData = encodeURIComponent(JSON.stringify(uniqueEndpoints)); return ` - ${modelName} stats + ${modelName} stats ${renderInstanceList(endpoints)} ${params} ${quant} @@ -1009,6 +1033,7 @@ function renderTimeSeriesChart(timeSeriesData, chart, minutes) { try { const payload = JSON.parse(e.data); // SSE sends plain text renderChart(payload); + updateTpsChart(payload); const usage = payload.usage_counts || {}; const tokens = payload.token_usage_counts || {}; @@ -1035,6 +1060,84 @@ function renderTimeSeriesChart(timeSeriesData, chart, minutes) { window.addEventListener("beforeunload", () => source.close()); } + /* ---------- Header TPS Chart ---------- */ + function initHeaderChart() { + const canvas = document.getElementById('header-tps-canvas'); + if (!canvas) return; + headerTpsChart = new Chart(canvas.getContext('2d'), { + type: 'line', + data: { labels: [], datasets: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + animation: false, + scales: { + x: { display: false }, + y: { display: true, min: 0, ticks: { font: { size: 10 } } } + }, + plugins: { + legend: { display: false }, + tooltip: { + callbacks: { + title: (items) => items[0]?.dataset?.label || '', + label: (item) => `${item.parsed.y.toFixed(1)} tok/s` + } + } + }, + elements: { point: { radius: 0 } } + } + }); + } + + function updateTpsChart(payload) { + const tokens = payload.token_usage_counts || {}; + const perModelTokens = {}; + psRows.forEach((_, model) => { + let total = 0; + for (const ep in tokens) total += tokens[ep]?.[model] || 0; + // Normalise against the first-seen cumulative total so history + // entries start at 0 and the || 0 fallback never causes a spike. + if (!(model in modelFirstSeen)) modelFirstSeen[model] = total; + perModelTokens[model] = total - modelFirstSeen[model]; + }); + latestPerModelTokens = perModelTokens; + } + + function tickTpsChart() { + if (!headerTpsChart) return; + tpsHistory.push({ time: Date.now(), perModelTokens: { ...latestPerModelTokens } }); + if (tpsHistory.length > TPS_HISTORY_SIZE) tpsHistory.shift(); + if (tpsHistory.length < 2) return; + + // Only chart models present in the latest snapshot — never accumulate + // stale names from old history entries. + const allModels = Object.keys(tpsHistory[tpsHistory.length - 1].perModelTokens); + + const labels = tpsHistory.map(h => new Date(h.time).toLocaleTimeString()); + const datasets = Array.from(allModels).map(model => { + const data = tpsHistory.map((h, i) => { + if (i === 0) return 0; + const prev = tpsHistory[i - 1]; + const dt = (h.time - prev.time) / 1000; + const dTokens = (h.perModelTokens[model] || 0) - (prev.perModelTokens[model] || 0); + return dt > 0 ? Math.max(0, dTokens / dt) : 0; + }); + return { + label: model, + data, + borderColor: getColor(model), + backgroundColor: 'transparent', + borderWidth: 2, + tension: 0.3, + pointRadius: 0 + }; + }); + + headerTpsChart.data.labels = labels; + headerTpsChart.data.datasets = datasets; + headerTpsChart.update('none'); + } + /* ---------- Init ---------- */ window.addEventListener("load", () => { updateApiKeyIndicator(); @@ -1068,6 +1171,8 @@ function renderTimeSeriesChart(timeSeriesData, chart, minutes) { loadTags(); loadPS(); loadUsage(); + initHeaderChart(); + setInterval(tickTpsChart, 1000); setInterval(loadPS, 60_000); setInterval(loadEndpoints, 300_000);