feat: prettyfy dashboard
This commit is contained in:
parent
c796fd6a47
commit
031de165a1
1 changed files with 110 additions and 5 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue