feat: prettyfy dashboard
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
Build and Publish Docker Image (Semantic Cache) / build-and-push-semantic (push) Has been cancelled

This commit is contained in:
Alpha Nerd 2026-03-27 16:24:57 +01:00
parent c796fd6a47
commit 031de165a1

View file

@ -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;
}
</style>
</head>
<body>
<a href="https://www.nomyo.ai" target="_blank"
><img src="./static/228394408.png" width="100px" height="100px"
/></a>
<div class="logo-chart-row">
<a href="https://www.nomyo.ai" target="_blank"
><img src="./static/228394408.png" width="100px" height="100px"
/></a>
<div id="header-tps-container">
<canvas id="header-tps-canvas"></canvas>
</div>
</div>
<div class="header-row">
<h1>Router Dashboard</h1>
<button id="total-tokens-btn">Stats Total</button>
@ -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 `<tr data-model="${modelName}" data-endpoints="${endpointsData}">
<td class="model">${modelName} <a href="#" class="stats-link" data-model="${modelName}">stats</a></td>
<td class="model"><span style="color:${getColor(modelName)}">${modelName}</span> <a href="#" class="stats-link" data-model="${modelName}">stats</a></td>
<td>${renderInstanceList(endpoints)}</td>
<td>${params}</td>
<td>${quant}</td>
@ -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);