// Analytics Page Module class AnalyticsPage { constructor() { this.filters = { dateRange: '7d', client: 'all', provider: 'all' }; this.init(); } async init() { // Load data await Promise.all([ this.loadClients(), this.loadCharts(), this.loadUsageData() ]); // Setup event listeners this.setupEventListeners(); } async loadClients() { try { const clients = await window.api.get('/clients'); this.renderClientFilter(clients); } catch (error) { console.error('Error loading clients for filter:', error); } } renderClientFilter(clients) { const select = document.getElementById('client-filter'); if (!select) return; // Clear existing options except "All Clients" while (select.options.length > 1) { select.remove(1); } // Add client options clients.forEach(client => { const option = document.createElement('option'); option.value = client.id; option.textContent = client.name || client.id; select.appendChild(option); }); } async loadCharts() { // Fetch each data source independently so one failure doesn't kill the others const [timeSeriesResult, breakdownResult] = await Promise.allSettled([ window.api.get('/usage/time-series'), window.api.get('/analytics/breakdown') ]); // Time-series chart if (timeSeriesResult.status === 'fulfilled') { const series = timeSeriesResult.value.series || []; if (series.length > 0) { this.renderAnalyticsChart(series); } else { this.showEmptyChart('analytics-chart', 'No request data in the last 24 hours'); } } else { console.error('Error loading time series:', timeSeriesResult.reason); this.showEmptyChart('analytics-chart', 'Failed to load request data'); } // Breakdown charts (clients + models) if (breakdownResult.status === 'fulfilled') { const breakdown = breakdownResult.value; const clients = breakdown.clients || []; const models = breakdown.models || []; if (clients.length > 0) { this.renderClientsChart(clients); } else { this.showEmptyChart('clients-chart', 'No client data available'); } if (models.length > 0) { this.renderModelsChart(models); } else { this.showEmptyChart('models-chart', 'No model data available'); } } else { console.error('Error loading analytics breakdown:', breakdownResult.reason); this.showEmptyChart('clients-chart', 'Failed to load client data'); this.showEmptyChart('models-chart', 'Failed to load model data'); } } showEmptyChart(canvasId, message) { const canvas = document.getElementById(canvasId); if (!canvas) return; const container = canvas.closest('.chart-container'); if (container) { canvas.style.display = 'none'; let msg = container.querySelector('.empty-chart-msg'); if (!msg) { msg = document.createElement('div'); msg.className = 'empty-chart-msg'; msg.style.cssText = 'display:flex;align-items:center;justify-content:center;height:200px;color:var(--fg4);font-size:0.9rem;'; container.appendChild(msg); } msg.textContent = message; } } renderAnalyticsChart(series) { const data = { labels: series.map(s => s.time), datasets: [ { label: 'Requests', data: series.map(s => s.requests), color: '#fe8019', // orange fill: true }, { label: 'Tokens', data: series.map(s => s.tokens), color: '#b8bb26', // green fill: true, hidden: true } ] }; window.chartManager.createLineChart('analytics-chart', data); } renderClientsChart(clients) { const data = { labels: clients.map(c => c.label), datasets: [{ label: 'Requests', data: clients.map(c => c.value), color: '#83a598' // blue }] }; window.chartManager.createHorizontalBarChart('clients-chart', data); } renderModelsChart(models) { const data = { labels: models.map(m => m.label), data: models.map(m => m.value), colors: window.chartManager.defaultColors }; window.chartManager.createDoughnutChart('models-chart', data); } async loadUsageData() { try { const usageData = await window.api.get('/usage/detailed'); this.renderUsageTable(usageData); } catch (error) { console.error('Error loading usage data:', error); } } renderUsageTable(data) { const tableBody = document.querySelector('#usage-table tbody'); if (!tableBody) return; if (data.length === 0) { tableBody.innerHTML = '
${row.model}