// Analytics Page Module class AnalyticsPage { constructor() { this.filters = { dateRange: '24h', client: 'all', provider: 'all' }; this.init(); } async init() { await Promise.all([ this.loadClients(), this.loadCharts(), this.loadUsageData() ]); this.setupEventListeners(); } /** Build query string from current period filter. */ periodQuery() { const p = this.filters.dateRange; if (p === 'custom') { const from = document.getElementById('analytics-from')?.value; const to = document.getElementById('analytics-to')?.value; let qs = '?period=custom'; if (from) qs += `&from=${from}T00:00:00Z`; if (to) qs += `&to=${to}T23:59:59Z`; return qs; } return `?period=${p}`; } 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; while (select.options.length > 1) { select.remove(1); } 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; } const qs = this.periodQuery(); const [timeSeriesResult, breakdownResult] = await Promise.allSettled([ window.api.get(`/usage/time-series${qs}`), window.api.get(`/analytics/breakdown${qs}`) ]); // Time-series chart if (timeSeriesResult.status === 'fulfilled') { const resp = timeSeriesResult.value; const series = resp.series || []; if (series.length > 0) { this.renderAnalyticsChart(series, resp.granularity); } else { this.showEmptyChart('analytics-chart', 'No request data for this period'); } } 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 for this period'); } if (models.length > 0) { this.renderModelsChart(models); } else { this.showEmptyChart('models-chart', 'No model data for this period'); } } 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, granularity) { const cm = window.chartManager; if (!cm) return; const labels = series.map(s => { // For daily granularity, shorten the date label if (granularity === 'day') { const d = luxon.DateTime.fromISO(s.time); return d.isValid ? d.toFormat('MMM dd') : s.time; } return s.time; }); const data = { labels, datasets: [ { label: 'Requests', data: series.map(s => s.requests), color: '#fe8019', fill: true }, { label: 'Tokens', data: series.map(s => s.tokens), color: '#b8bb26', 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' }] }; 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 qs = this.periodQuery(); const usageData = await window.api.get(`/usage/detailed${qs}`); 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 usage data for this period'; 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(); } const exportBtn = document.getElementById('export-data'); if (exportBtn) { exportBtn.onclick = () => this.exportData(); } // Period selector buttons const periodContainer = document.getElementById('analytics-period'); if (periodContainer) { periodContainer.querySelectorAll('[data-period]').forEach(btn => { btn.onclick = () => { // Toggle active class periodContainer.querySelectorAll('[data-period]').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const period = btn.dataset.period; this.filters.dateRange = period; // Show/hide custom range inputs const customRange = document.getElementById('analytics-custom-range'); if (customRange) { customRange.style.display = period === 'custom' ? 'flex' : 'none'; } if (period !== 'custom') { this.refresh(); } }; }); } // Custom range apply button const applyCustom = document.getElementById('analytics-apply-custom'); if (applyCustom) { applyCustom.onclick = () => this.refresh(); } } async exportData() { const qs = this.periodQuery(); const data = await window.api.get(`/usage/detailed${qs}`); 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(); };