// 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() { 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 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 cm = window.chartManager; if (!cm) return; 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 } ] }; cm.createLineChart('analytics-chart', data); } renderClientsChart(clients) { const cm = window.chartManager; if (!cm) return; const data = { labels: clients.map(c => c.label), datasets: [{ label: 'Requests', data: clients.map(c => c.value), color: '#83a598' // blue }] }; cm.createHorizontalBarChart('clients-chart', data); } renderModelsChart(models) { const cm = window.chartManager; if (!cm) return; const data = { labels: models.map(m => m.label), data: models.map(m => m.value), colors: cm.defaultColors }; cm.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 = 'No historical data found'; return; } tableBody.innerHTML = data.map(row => { const cacheRead = row.cache_read_tokens || 0; const cacheWrite = row.cache_write_tokens || 0; return ` ${row.date} ${row.client} ${row.provider} ${row.model} ${row.requests.toLocaleString()} ${window.api.formatNumber(row.tokens)} ${window.api.formatNumber(cacheRead)} ${window.api.formatNumber(cacheWrite)} ${window.api.formatCurrency(row.cost)} `; }).join(''); } setupEventListeners() { const refreshBtn = document.getElementById('refresh-analytics'); if (refreshBtn) { refreshBtn.onclick = () => this.refresh(); } // Export button const exportBtn = document.getElementById('export-data'); if (exportBtn) { exportBtn.onclick = () => this.exportData(); } } async exportData() { // Simple CSV export const data = await window.api.get('/usage/detailed'); if (!data || data.length === 0) return; const headers = Object.keys(data[0]).join(','); const rows = data.map(obj => Object.values(obj).join(',')).join('\n'); const csv = `${headers}\n${rows}`; const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `llm-proxy-usage-${new Date().toISOString().split('T')[0]}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } refresh() { this.loadCharts(); this.loadUsageData(); window.authManager.showToast('Analytics data refreshed', 'success'); } } window.initAnalytics = async () => { window.analyticsPage = new AnalyticsPage(); };