initial chart view

This commit is contained in:
Alpha Nerd 2025-11-19 17:05:25 +01:00
parent 541f2826e0
commit 79a7ca972b
3 changed files with 208 additions and 33 deletions

0
entrypoint.sh Normal file → Executable file
View file

View file

@ -1193,9 +1193,10 @@ async def stats_proxy(request: Request, model: Optional[str] = None):
status_code=404, detail="No token data found for this model"
)
# Get time series data
# Get time series data for the last 30 days (43200 minutes = 30 days)
# Assuming entries are grouped by minute, 30 days = 43200 entries max
time_series = []
async for entry in db.get_latest_time_series(limit=10):
async for entry in db.get_latest_time_series(limit=50000):
if entry['model'] == model:
time_series.append({
'timestamp': entry['timestamp'],

View file

@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<title>NOMYO Router Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
@ -81,7 +82,7 @@
margin-left: 0.5rem;
margin-right: 0.5rem;
min-width: 30%;
font-size: 1rem;
transition: 0.3s;
}
@ -152,14 +153,14 @@
align-items: center;
justify-content: center;
}
.modal-content {
background: #fff;
padding: 1rem;
max-width: 90%;
max-height: 90%;
overflow: auto;
border-radius: 6px;
}
.modal-content {
background: #fff;
padding: 1rem;
width: 95%;
height: 95%;
overflow: auto;
border-radius: 6px;
}
.close-btn {
float: right;
cursor: pointer;
@ -210,6 +211,27 @@
order: 1;
}
}
/* ---------- Chart Timeframe Controls ---------- */
.timeframe-controls {
margin: 1rem 0;
}
.timeframe-controls button {
margin-right: 0.5rem;
padding: 0.25rem 0.5rem;
cursor: pointer;
background-color: #e0e0e0;
border: none;
border-radius: 4px;
}
.timeframe-controls button.active {
background-color: #0066cc;
color: white;
}
.chart-container {
position: relative;
height: 300px;
margin-top: 1rem;
}
</style>
</head>
<body>
@ -267,7 +289,7 @@
<th>Quant</th>
<th>Ctx</th>
<th>Digest</th>
<th>Token</th>
<th>Token</th>
</tr>
</thead>
<tbody id="ps-body">
@ -300,7 +322,97 @@
</div>
<script>
let psRows = new Map();
let psRows = new Map();
// Global placeholders for stats modal handling
let statsModal = null; // stats modal element
let statsChart = null; // Chart.js instance inside the modal
let rawTimeSeries = null; // raw timeseries data for the current model
/* Integrated modal initialization and close handling into the main load block */
// Assign the stats modal element and attach the close handler once the DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Get the modal element (it now exists in the DOM)
statsModal = document.getElementById('stats-modal');
// Attach a single close handler (prevents multiple duplicate listeners)
if (statsModal) {
statsModal.addEventListener('click', (e) => {
if (e.target === statsModal || e.target.matches('.close-btn')) {
// Hide the modal
statsModal.style.display = 'none';
// Clean up the chart instance to avoid caching stale data
if (statsChart) {
statsChart.destroy();
statsChart = null;
}
// Remove the canvas element so a fresh one is created on next open
const oldCanvas = document.getElementById('time-series-chart');
if (oldCanvas) {
oldCanvas.remove();
}
// Reset stored timeseries data to avoid reuse of stale data
rawTimeSeries = null;
}
});
}
});
/* ---------- Global renderTimeSeriesChart ---------- */
function renderTimeSeriesChart(timeSeriesData, chart, minutes) {
console.log('renderTimeSeriesChart called with minutes:', minutes, 'data length:', timeSeriesData?.length);
// Safety check
if (!timeSeriesData || !Array.isArray(timeSeriesData)) {
console.warn('No valid time series data provided');
chart.data.labels = [];
chart.data.datasets[0].data = [];
chart.data.datasets[1].data = [];
chart.update();
return;
}
// Filter data based on selected timeframe
const now = new Date();
const cutoffTime = now.getTime() - (minutes * 60 * 1000);
// Group data by hour for better visualization
const groupedData = {};
timeSeriesData.forEach(item => {
const timestamp = item.timestamp * 1000;
if (timestamp >= cutoffTime) {
const date = new Date(timestamp);
// Group by hour (local time)
const hourStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
if (!groupedData[hourStr]) {
groupedData[hourStr] = {
input: 0,
output: 0
};
}
groupedData[hourStr].input += item.input_tokens || 0;
groupedData[hourStr].output += item.output_tokens || 0;
}
});
// Convert to arrays for chart
const labels = Object.keys(groupedData).sort();
const inputData = labels.map(hour => groupedData[hour].input);
const outputData = labels.map(hour => groupedData[hour].output);
console.log('Chart updated with', labels.length, 'data points');
// Update chart data
chart.data.labels = labels;
chart.data.datasets[0].data = inputData;
chart.data.datasets[1].data = outputData;
chart.update();
}
/* ---------- Utility ---------- */
async function fetchJSON(url) {
const resp = await fetch(url);
@ -565,7 +677,9 @@
loadUsage();
setInterval(loadPS, 60_000);
setInterval(loadEndpoints, 300_000);
/* (renderTimeSeriesChart removed will be defined globally) */
/* show logic */
document.body.addEventListener("click", async (e) => {
if (!e.target.matches(".show-link")) return;
@ -657,35 +771,95 @@
<p>Output tokens: ${data.output_tokens}</p>
<p>Total tokens: ${data.total_tokens}</p>
<h3>Usage Over Time</h3>
<div id="time-series-chart">
${data.time_series.length > 0 ?
data.time_series.map(ts => `
<div>
<strong>${new Date(ts.timestamp * 1000).toLocaleString()}</strong>
<p>Input: ${ts.input_tokens}, Output: ${ts.output_tokens}, Total: ${ts.total_tokens}</p>
</div>
`).join('') :
'<p>No time series data available</p>'
}
<div class="timeframe-controls">
<button class="timeframe-btn active" data-minutes="60">Last 1 hour</button>
<button class="timeframe-btn" data-minutes="1440">Last 1 day</button>
<button class="timeframe-btn" data-minutes="10080">Last 7 days</button>
<button class="timeframe-btn" data-minutes="43200">Last 30 days</button>
</div>
<div class="chart-container">
<canvas id="time-series-chart"></canvas>
</div>
`;
document.getElementById("stats-modal").style.display = "flex";
// Initialise the chart (ensures fresh canvas and chart instance)
initStatsChart(data.time_series);
} catch (err) {
console.error(err);
alert(`Could not load model stats: ${err.message}`);
}
});
/* ---------- Helper to initialise or refresh the stats chart ---------- */
function initStatsChart(timeSeriesData) {
console.log('initStatsChart called with payload:', timeSeriesData);
// Destroy any existing chart instance
if (statsChart) {
statsChart.destroy();
statsChart = null;
}
// Remove any existing canvas and create a fresh one
const oldCanvas = document.getElementById('time-series-chart');
if (oldCanvas) {
oldCanvas.remove();
}
const canvas = document.createElement('canvas');
canvas.id = 'time-series-chart';
document.querySelector('.chart-container').appendChild(canvas);
// Create a new Chart.js instance
const ctx = canvas.getContext('2d');
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: [],
datasets: [
{ label: 'Input Tokens', data: [], backgroundColor: '#4CAF50' },
{ label: 'Output Tokens', data: [], backgroundColor: '#2196F3' }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { stacked: true },
y: { stacked: true }
},
plugins: {
legend: { position: 'top' },
title: { display: true, text: 'Token Usage Over Time' }
}
}
});
// Store the chart globally for later updates
statsChart = chart;
// Store the raw timeseries data globally
console.log('initStatsChart raw data:', timeSeriesData);
rawTimeSeries = timeSeriesData || [];
// Render the initial view (default to 60minutes)
renderTimeSeriesChart(rawTimeSeries, statsChart, 60);
// Attach timeframe button handlers (once)
document.querySelectorAll('.timeframe-btn').forEach(button => {
button.addEventListener('click', function () {
// Update active button styling
document.querySelectorAll('.timeframe-btn').forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// Rerender chart with the selected timeframe
const minutes = parseInt(this.dataset.minutes);
renderTimeSeriesChart(rawTimeSeries, statsChart, minutes);
});
});
}
/* stats modal close */
const statsModal = document.getElementById("stats-modal");
statsModal.addEventListener("click", (e) => {
if (
e.target === statsModal ||
e.target.matches(".close-btn")
) {
statsModal.style.display = "none";
}
});
// The close handler is already attached during initial page load.
// No additional listener is needed here.
});
</script>