fix(dashboard): add cache-busting and defensive chartManager guards to fix empty charts
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

This commit is contained in:
2026-03-02 13:28:29 -05:00
parent 8d50ce7c22
commit 1766a12ea2
8 changed files with 88 additions and 34 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LLM Proxy Gateway - Admin Dashboard</title> <title>LLM Proxy Gateway - Admin Dashboard</title>
<link rel="stylesheet" href="/css/dashboard.css"> <link rel="stylesheet" href="/css/dashboard.css?v=3">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="icon" href="img/logo-icon.png" type="image/png" sizes="any"> <link rel="icon" href="img/logo-icon.png" type="image/png" sizes="any">
<link rel="apple-touch-icon" href="img/logo-icon.png"> <link rel="apple-touch-icon" href="img/logo-icon.png">
@@ -165,20 +165,20 @@
</main> </main>
</div> </div>
<!-- Scripts --> <!-- Scripts (cache-busted with version query params) -->
<script src="/js/api.js"></script> <script src="/js/api.js?v=3"></script>
<script src="/js/auth.js"></script> <script src="/js/auth.js?v=3"></script>
<script src="/js/dashboard.js"></script> <script src="/js/dashboard.js?v=3"></script>
<script src="/js/websocket.js"></script> <script src="/js/websocket.js?v=3"></script>
<script src="/js/charts.js"></script> <script src="/js/charts.js?v=3"></script>
<script src="/js/pages/overview.js"></script> <script src="/js/pages/overview.js?v=3"></script>
<script src="/js/pages/analytics.js"></script> <script src="/js/pages/analytics.js?v=3"></script>
<script src="/js/pages/costs.js"></script> <script src="/js/pages/costs.js?v=3"></script>
<script src="/js/pages/clients.js"></script> <script src="/js/pages/clients.js?v=3"></script>
<script src="/js/pages/providers.js"></script> <script src="/js/pages/providers.js?v=3"></script>
<script src="/js/pages/models.js"></script> <script src="/js/pages/models.js?v=3"></script>
<script src="/js/pages/monitoring.js"></script> <script src="/js/pages/monitoring.js?v=3"></script>
<script src="/js/pages/settings.js"></script> <script src="/js/pages/settings.js?v=3"></script>
<script src="/js/pages/logs.js"></script> <script src="/js/pages/logs.js?v=3"></script>
</body> </body>
</html> </html>

View File

@@ -88,3 +88,15 @@ class ApiClient {
} }
window.api = new ApiClient(); window.api = new ApiClient();
// Helper: waits until chartManager is available (defensive against load-order edge cases)
window.waitForChartManager = async (timeoutMs = 3000) => {
if (window.chartManager) return window.chartManager;
const start = Date.now();
while (Date.now() - start < timeoutMs) {
await new Promise(r => setTimeout(r, 50));
if (window.chartManager) return window.chartManager;
}
console.warn('chartManager not available after', timeoutMs, 'ms');
return null;
};

View File

@@ -79,8 +79,9 @@ class Dashboard {
} }
async loadPage(page) { async loadPage(page) {
if (window.chartManager) { const cm = window.chartManager;
window.chartManager.destroyAllCharts(); if (cm) {
cm.destroyAllCharts();
} }
this.currentPage = page; this.currentPage = page;

View File

@@ -50,6 +50,14 @@ class AnalyticsPage {
} }
async loadCharts() { async loadCharts() {
const cm = window.chartManager || await window.waitForChartManager();
if (!cm) {
this.showEmptyChart('analytics-chart', 'Chart system unavailable');
this.showEmptyChart('clients-chart', 'Chart system unavailable');
this.showEmptyChart('models-chart', 'Chart system unavailable');
return;
}
// Fetch each data source independently so one failure doesn't kill the others // Fetch each data source independently so one failure doesn't kill the others
const [timeSeriesResult, breakdownResult] = await Promise.allSettled([ const [timeSeriesResult, breakdownResult] = await Promise.allSettled([
window.api.get('/usage/time-series'), window.api.get('/usage/time-series'),
@@ -111,6 +119,8 @@ class AnalyticsPage {
} }
renderAnalyticsChart(series) { renderAnalyticsChart(series) {
const cm = window.chartManager;
if (!cm) return;
const data = { const data = {
labels: series.map(s => s.time), labels: series.map(s => s.time),
datasets: [ datasets: [
@@ -130,10 +140,12 @@ class AnalyticsPage {
] ]
}; };
window.chartManager.createLineChart('analytics-chart', data); cm.createLineChart('analytics-chart', data);
} }
renderClientsChart(clients) { renderClientsChart(clients) {
const cm = window.chartManager;
if (!cm) return;
const data = { const data = {
labels: clients.map(c => c.label), labels: clients.map(c => c.label),
datasets: [{ datasets: [{
@@ -143,17 +155,19 @@ class AnalyticsPage {
}] }]
}; };
window.chartManager.createHorizontalBarChart('clients-chart', data); cm.createHorizontalBarChart('clients-chart', data);
} }
renderModelsChart(models) { renderModelsChart(models) {
const cm = window.chartManager;
if (!cm) return;
const data = { const data = {
labels: models.map(m => m.label), labels: models.map(m => m.label),
data: models.map(m => m.value), data: models.map(m => m.value),
colors: window.chartManager.defaultColors colors: cm.defaultColors
}; };
window.chartManager.createDoughnutChart('models-chart', data); cm.createDoughnutChart('models-chart', data);
} }
async loadUsageData() { async loadUsageData() {

View File

@@ -75,6 +75,9 @@ class ClientsPage {
async loadClientUsageChart() { async loadClientUsageChart() {
try { try {
const cm = window.chartManager || await window.waitForChartManager();
if (!cm) { this.showEmptyChart('client-usage-chart', 'Chart system unavailable'); return; }
const data = await window.api.get('/usage/clients'); const data = await window.api.get('/usage/clients');
if (!data || data.length === 0) { if (!data || data.length === 0) {
@@ -91,7 +94,7 @@ class ClientsPage {
}] }]
}; };
window.chartManager.createHorizontalBarChart('client-usage-chart', chartData); cm.createHorizontalBarChart('client-usage-chart', chartData);
} catch (error) { } catch (error) {
console.error('Error loading client usage chart:', error); console.error('Error loading client usage chart:', error);

View File

@@ -91,6 +91,9 @@ class CostsPage {
async loadCostsChart() { async loadCostsChart() {
try { try {
const cm = window.chartManager || await window.waitForChartManager();
if (!cm) { this.showEmptyChart('costs-chart', 'Chart system unavailable'); return; }
const data = await window.api.get('/usage/providers'); const data = await window.api.get('/usage/providers');
if (!data || data.length === 0) { if (!data || data.length === 0) {
@@ -107,7 +110,7 @@ class CostsPage {
}] }]
}; };
window.chartManager.createBarChart('costs-chart', chartData); cm.createBarChart('costs-chart', chartData);
} catch (error) { } catch (error) {
console.error('Error loading costs chart:', error); console.error('Error loading costs chart:', error);

View File

@@ -153,6 +153,13 @@ class MonitoringPage {
} }
async loadCharts() { async loadCharts() {
// Ensure chartManager is available
const cm = window.chartManager || await window.waitForChartManager();
if (!cm) {
console.warn('chartManager unavailable, skipping monitoring charts');
return;
}
// Fetch recent logs for chart data // Fetch recent logs for chart data
try { try {
const logs = await window.api.get('/system/logs'); const logs = await window.api.get('/system/logs');
@@ -168,6 +175,8 @@ class MonitoringPage {
async loadResponseTimeChart() { async loadResponseTimeChart() {
try { try {
const cm = window.chartManager;
if (!cm) return;
// Bucket recent logs by minute for latency chart // Bucket recent logs by minute for latency chart
const buckets = this.bucketByMinute(this.recentLogs, 20); const buckets = this.bucketByMinute(this.recentLogs, 20);
const labels = buckets.map(b => b.label); const labels = buckets.map(b => b.label);
@@ -187,7 +196,7 @@ class MonitoringPage {
}] }]
}; };
window.chartManager.createLineChart('response-time-chart', data, { cm.createLineChart('response-time-chart', data, {
scales: { scales: {
y: { y: {
title: { display: true, text: 'Milliseconds' }, title: { display: true, text: 'Milliseconds' },
@@ -202,6 +211,8 @@ class MonitoringPage {
async loadErrorRateChart() { async loadErrorRateChart() {
try { try {
const cm = window.chartManager;
if (!cm) return;
const buckets = this.bucketByMinute(this.recentLogs, 20); const buckets = this.bucketByMinute(this.recentLogs, 20);
const labels = buckets.map(b => b.label); const labels = buckets.map(b => b.label);
const values = buckets.map(b => { const values = buckets.map(b => {
@@ -220,7 +231,7 @@ class MonitoringPage {
}] }]
}; };
window.chartManager.createLineChart('error-rate-chart', data, { cm.createLineChart('error-rate-chart', data, {
scales: { scales: {
y: { y: {
title: { display: true, text: 'Percentage' }, title: { display: true, text: 'Percentage' },
@@ -238,6 +249,8 @@ class MonitoringPage {
async loadRateLimitChart() { async loadRateLimitChart() {
try { try {
const cm = window.chartManager;
if (!cm) return;
// Show requests-per-client from recent logs // Show requests-per-client from recent logs
const clientCounts = {}; const clientCounts = {};
for (const log of this.recentLogs) { for (const log of this.recentLogs) {
@@ -257,7 +270,7 @@ class MonitoringPage {
}] }]
}; };
window.chartManager.createBarChart('rate-limit-chart', data, { cm.createBarChart('rate-limit-chart', data, {
scales: { scales: {
y: { y: {
title: { display: true, text: 'Request Count' }, title: { display: true, text: 'Request Count' },
@@ -452,22 +465,24 @@ class MonitoringPage {
} }
updateCharts(metric) { updateCharts(metric) {
const cm = window.chartManager;
if (!cm) return;
// Update charts with new metric data // Update charts with new metric data
if (metric.type === 'response_time' && window.chartManager.charts.has('response-time-chart')) { if (metric.type === 'response_time' && cm.charts.has('response-time-chart')) {
this.updateResponseTimeChart(metric.value); this.updateResponseTimeChart(metric.value);
} }
if (metric.type === 'error_rate' && window.chartManager.charts.has('error-rate-chart')) { if (metric.type === 'error_rate' && cm.charts.has('error-rate-chart')) {
this.updateErrorRateChart(metric.value); this.updateErrorRateChart(metric.value);
} }
} }
updateResponseTimeChart(value) { updateResponseTimeChart(value) {
window.chartManager.addDataPoint('response-time-chart', value); if (window.chartManager) window.chartManager.addDataPoint('response-time-chart', value);
} }
updateErrorRateChart(value) { updateErrorRateChart(value) {
window.chartManager.addDataPoint('error-rate-chart', value); if (window.chartManager) window.chartManager.addDataPoint('error-rate-chart', value);
} }
startDemoUpdates() { startDemoUpdates() {

View File

@@ -128,6 +128,9 @@ class OverviewPage {
async loadRequestsChart() { async loadRequestsChart() {
try { try {
const cm = window.chartManager || await window.waitForChartManager();
if (!cm) { this.showEmptyChart('requests-chart', 'Chart system unavailable'); return; }
const data = await window.api.get('/usage/time-series'); const data = await window.api.get('/usage/time-series');
const series = data.series || []; const series = data.series || [];
@@ -148,7 +151,7 @@ class OverviewPage {
] ]
}; };
this.charts.requests = window.chartManager.createLineChart('requests-chart', chartData); this.charts.requests = cm.createLineChart('requests-chart', chartData);
} catch (error) { } catch (error) {
console.error('Error loading requests chart:', error); console.error('Error loading requests chart:', error);
this.showEmptyChart('requests-chart', 'Failed to load request data'); this.showEmptyChart('requests-chart', 'Failed to load request data');
@@ -157,6 +160,9 @@ class OverviewPage {
async loadProvidersChart() { async loadProvidersChart() {
try { try {
const cm = window.chartManager || await window.waitForChartManager();
if (!cm) { this.showEmptyChart('providers-chart', 'Chart system unavailable'); return; }
const data = await window.api.get('/usage/providers'); const data = await window.api.get('/usage/providers');
if (!data || data.length === 0) { if (!data || data.length === 0) {
@@ -167,10 +173,10 @@ class OverviewPage {
const chartData = { const chartData = {
labels: data.map(item => item.provider), labels: data.map(item => item.provider),
data: data.map(item => item.requests), data: data.map(item => item.requests),
colors: data.map((_, i) => window.chartManager.defaultColors[i % window.chartManager.defaultColors.length]) colors: data.map((_, i) => cm.defaultColors[i % cm.defaultColors.length])
}; };
this.charts.providers = window.chartManager.createDoughnutChart('providers-chart', chartData); this.charts.providers = cm.createDoughnutChart('providers-chart', chartData);
} catch (error) { } catch (error) {
console.error('Error loading providers chart:', error); console.error('Error loading providers chart:', error);
this.showEmptyChart('providers-chart', 'Failed to load provider data'); this.showEmptyChart('providers-chart', 'Failed to load provider data');