initial chart view
This commit is contained in:
parent
541f2826e0
commit
79a7ca972b
3 changed files with 208 additions and 33 deletions
0
entrypoint.sh
Normal file → Executable file
0
entrypoint.sh
Normal file → Executable 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'],
|
||||
|
|
|
|||
|
|
@ -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 time‑series 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 time‑series 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 time‑series data globally
|
||||
console.log('initStatsChart raw data:', timeSeriesData);
|
||||
rawTimeSeries = timeSeriesData || [];
|
||||
|
||||
// Render the initial view (default to 60 minutes)
|
||||
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');
|
||||
|
||||
// Re‑render 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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue