feat: implement analytics and cost management dashboard pages

This commit is contained in:
2026-02-26 18:37:37 -05:00
parent 25986dd255
commit 519436eb4a
4 changed files with 309 additions and 582 deletions

View File

@@ -11,31 +11,23 @@ class AnalyticsPage {
}
async init() {
// Load initial data
await this.loadFilters();
await this.loadCharts();
await this.loadUsageData();
// Load data
await Promise.all([
this.loadClients(),
this.loadCharts(),
this.loadUsageData()
]);
// Setup event listeners
this.setupEventListeners();
}
async loadFilters() {
async loadClients() {
try {
// Load clients for filter dropdown
// In a real app, this would fetch from /api/clients
const clients = [
{ id: 'client-1', name: 'Web Application' },
{ id: 'client-2', name: 'Mobile App' },
{ id: 'client-3', name: 'API Integration' },
{ id: 'client-4', name: 'Internal Tools' },
{ id: 'client-5', name: 'Testing Suite' }
];
const clients = await window.api.get('/clients');
this.renderClientFilter(clients);
} catch (error) {
console.error('Error loading filters:', error);
console.error('Error loading clients for filter:', error);
}
}
@@ -52,114 +44,74 @@ class AnalyticsPage {
clients.forEach(client => {
const option = document.createElement('option');
option.value = client.id;
option.textContent = client.name;
option.textContent = client.name || client.id;
select.appendChild(option);
});
}
async loadCharts() {
await this.loadAnalyticsChart();
await this.loadClientsChart();
await this.loadModelsChart();
}
async loadAnalyticsChart() {
try {
// Generate demo data
const labels = window.chartManager.generateDateLabels(7);
const data = {
labels: labels,
datasets: [
{
label: 'Requests',
data: labels.map(() => Math.floor(Math.random() * 1000) + 500),
color: '#3b82f6',
fill: true
},
{
label: 'Tokens',
data: labels.map(() => Math.floor(Math.random() * 100000) + 50000),
color: '#10b981',
fill: true
},
{
label: 'Cost ($)',
data: labels.map(() => Math.random() * 50 + 10),
color: '#f59e0b',
fill: true
}
]
};
// Create chart
window.chartManager.createLineChart('analytics-chart', data, {
scales: {
y: {
ticks: {
callback: function(value) {
return value.toLocaleString();
}
}
}
}
});
const breakdown = await window.api.get('/analytics/breakdown');
const timeSeries = await window.api.get('/usage/time-series');
this.renderAnalyticsChart(timeSeries.series);
this.renderClientsChart(breakdown.clients);
this.renderModelsChart(breakdown.models);
} catch (error) {
console.error('Error loading analytics chart:', error);
console.error('Error loading analytics charts:', error);
}
}
async loadClientsChart() {
try {
const data = {
labels: ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'],
datasets: [{
renderAnalyticsChart(series) {
const data = {
labels: series.map(s => s.time),
datasets: [
{
label: 'Requests',
data: [45, 25, 15, 10, 5],
color: '#3b82f6'
}]
};
window.chartManager.createHorizontalBarChart('clients-chart', data);
} catch (error) {
console.error('Error loading clients chart:', error);
}
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);
}
async loadModelsChart() {
try {
const data = {
labels: ['gpt-4', 'gpt-3.5-turbo', 'gemini-pro', 'deepseek-chat', 'grok-beta'],
data: [35, 30, 20, 10, 5],
colors: ['#3b82f6', '#60a5fa', '#10b981', '#f59e0b', '#8b5cf6']
};
window.chartManager.createDoughnutChart('models-chart', data);
} catch (error) {
console.error('Error loading models chart:', error);
}
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 {
// In a real app, this would fetch from /api/usage/detailed
const usageData = [
{ date: '2024-01-15', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 245, tokens: 125000, cost: 12.50 },
{ date: '2024-01-15', client: 'Mobile App', provider: 'Gemini', model: 'gemini-pro', requests: 180, tokens: 89000, cost: 8.90 },
{ date: '2024-01-15', client: 'API Integration', provider: 'OpenAI', model: 'gpt-3.5-turbo', requests: 320, tokens: 156000, cost: 15.60 },
{ date: '2024-01-14', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 210, tokens: 110000, cost: 11.00 },
{ date: '2024-01-14', client: 'Internal Tools', provider: 'DeepSeek', model: 'deepseek-chat', requests: 95, tokens: 48000, cost: 4.80 },
{ date: '2024-01-14', client: 'Testing Suite', provider: 'Grok', model: 'grok-beta', requests: 45, tokens: 22000, cost: 2.20 },
{ date: '2024-01-13', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 195, tokens: 98000, cost: 9.80 },
{ date: '2024-01-13', client: 'Mobile App', provider: 'Gemini', model: 'gemini-pro', requests: 165, tokens: 82000, cost: 8.20 },
{ date: '2024-01-13', client: 'API Integration', provider: 'OpenAI', model: 'gpt-3.5-turbo', requests: 285, tokens: 142000, cost: 14.20 },
{ date: '2024-01-12', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 230, tokens: 118000, cost: 11.80 }
];
const usageData = await window.api.get('/usage/detailed');
this.renderUsageTable(usageData);
} catch (error) {
console.error('Error loading usage data:', error);
}
@@ -169,166 +121,63 @@ class AnalyticsPage {
const tableBody = document.querySelector('#usage-table tbody');
if (!tableBody) return;
if (data.length === 0) {
tableBody.innerHTML = '<tr><td colspan="7" class="text-center">No historical data found</td></tr>';
return;
}
tableBody.innerHTML = data.map(row => `
<tr>
<td>${row.date}</td>
<td>${row.client}</td>
<td><span class="badge-client">${row.client}</span></td>
<td>${row.provider}</td>
<td>${row.model}</td>
<td><code class="code-sm">${row.model}</code></td>
<td>${row.requests.toLocaleString()}</td>
<td>${row.tokens.toLocaleString()}</td>
<td>$${row.cost.toFixed(2)}</td>
<td>${window.api.formatNumber(row.tokens)}</td>
<td>${window.api.formatCurrency(row.cost)}</td>
</tr>
`).join('');
}
setupEventListeners() {
// Filter controls
const dateRangeSelect = document.getElementById('date-range');
const clientSelect = document.getElementById('client-filter');
const providerSelect = document.getElementById('provider-filter');
if (dateRangeSelect) {
dateRangeSelect.addEventListener('change', (e) => {
this.filters.dateRange = e.target.value;
this.applyFilters();
});
const refreshBtn = document.getElementById('refresh-analytics');
if (refreshBtn) {
refreshBtn.onclick = () => this.refresh();
}
if (clientSelect) {
clientSelect.addEventListener('change', (e) => {
this.filters.client = e.target.value;
this.applyFilters();
});
}
if (providerSelect) {
providerSelect.addEventListener('change', (e) => {
this.filters.provider = e.target.value;
this.applyFilters();
});
}
// Chart metric buttons
const metricButtons = document.querySelectorAll('.chart-control-btn[data-metric]');
metricButtons.forEach(button => {
button.addEventListener('click', () => {
// Update active state
metricButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Update chart based on metric
this.updateAnalyticsChart(button.dataset.metric);
});
});
// Export button
const exportBtn = document.querySelector('#analytics .btn-secondary');
const exportBtn = document.getElementById('export-data');
if (exportBtn) {
exportBtn.addEventListener('click', () => {
this.exportData();
});
exportBtn.onclick = () => this.exportData();
}
}
applyFilters() {
console.log('Applying filters:', this.filters);
// In a real app, this would fetch filtered data from the API
// For now, just show a toast
if (window.authManager) {
window.authManager.showToast('Filters applied', 'success');
}
// Refresh data
this.loadCharts();
this.loadUsageData();
}
async exportData() {
// Simple CSV export
const data = await window.api.get('/usage/detailed');
if (!data || data.length === 0) return;
updateAnalyticsChart(metric) {
// Update the main analytics chart to show the selected metric
const labels = window.chartManager.generateDateLabels(7);
let data;
if (metric === 'requests') {
data = {
labels: labels,
datasets: [{
label: 'Requests',
data: labels.map(() => Math.floor(Math.random() * 1000) + 500),
color: '#3b82f6',
fill: true
}]
};
} else if (metric === 'tokens') {
data = {
labels: labels,
datasets: [{
label: 'Tokens',
data: labels.map(() => Math.floor(Math.random() * 100000) + 50000),
color: '#10b981',
fill: true
}]
};
} else if (metric === 'cost') {
data = {
labels: labels,
datasets: [{
label: 'Cost ($)',
data: labels.map(() => Math.random() * 50 + 10),
color: '#f59e0b',
fill: true
}]
};
}
window.chartManager.updateChartData('analytics-chart', data);
}
const headers = Object.keys(data[0]).join(',');
const rows = data.map(obj => Object.values(obj).join(',')).join('\n');
const csv = `${headers}\n${rows}`;
exportData() {
// Create CSV data
const table = document.getElementById('usage-table');
if (!table) return;
const rows = table.querySelectorAll('tr');
const csv = [];
rows.forEach(row => {
const rowData = [];
row.querySelectorAll('th, td').forEach(cell => {
rowData.push(`"${cell.textContent.replace(/"/g, '""')}"`);
});
csv.push(rowData.join(','));
});
// Create download link
const blob = new Blob([csv.join('\n')], { type: 'text/csv' });
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-analytics-${new Date().toISOString().split('T')[0]}.csv`;
a.download = `llm-proxy-usage-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Show success message
if (window.authManager) {
window.authManager.showToast('Data exported successfully', 'success');
}
}
refresh() {
this.loadCharts();
this.loadUsageData();
window.authManager.showToast('Analytics data refreshed', 'success');
}
}
// Initialize analytics page when needed
window.initAnalytics = async () => {
window.analyticsPage = new AnalyticsPage();
};
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = AnalyticsPage;
}