feat: major dashboard overhaul and polish

- Switched from mock data to real backend APIs.
- Implemented unified ApiClient for consistent frontend data fetching.
- Refactored dashboard structure and styles for a modern SaaS aesthetic.
- Fixed Axum 0.8+ routing and parameter syntax issues.
- Implemented real client creation/deletion and provider health monitoring.
- Synchronized WebSocket event structures between backend and frontend.
This commit is contained in:
2026-02-26 15:40:12 -05:00
parent 888b0e71c4
commit 686163780c
11 changed files with 963 additions and 3563 deletions

View File

@@ -9,9 +9,11 @@ class OverviewPage {
async init() {
// Load data
await this.loadStats();
await this.loadCharts();
await this.loadRecentRequests();
await Promise.all([
this.loadStats(),
this.loadCharts(),
this.loadRecentRequests()
]);
// Setup event listeners
this.setupEventListeners();
@@ -22,21 +24,9 @@ class OverviewPage {
async loadStats() {
try {
// In a real app, this would fetch from /api/usage/summary
// For now, use mock data
this.stats = {
totalRequests: 12458,
totalTokens: 1254300,
totalCost: 125.43,
activeClients: 8,
errorRate: 2.3,
avgResponseTime: 450,
todayRequests: 342,
todayCost: 12.45
};
const data = await window.api.get('/usage/summary');
this.stats = data;
this.renderStats();
} catch (error) {
console.error('Error loading stats:', error);
this.showError('Failed to load statistics');
@@ -45,7 +35,7 @@ class OverviewPage {
renderStats() {
const container = document.getElementById('overview-stats');
if (!container) return;
if (!container || !this.stats) return;
container.innerHTML = `
<div class="stat-card">
@@ -53,11 +43,10 @@ class OverviewPage {
<i class="fas fa-exchange-alt"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.stats.totalRequests.toLocaleString()}</div>
<div class="stat-value">${this.stats.total_requests.toLocaleString()}</div>
<div class="stat-label">Total Requests</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i>
${this.stats.todayRequests} today
<div class="stat-change ${this.stats.today_requests > 0 ? 'positive' : ''}">
${this.stats.today_requests > 0 ? `<i class="fas fa-arrow-up"></i> ${this.stats.today_requests} today` : 'No requests today'}
</div>
</div>
</div>
@@ -67,11 +56,10 @@ class OverviewPage {
<i class="fas fa-coins"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.stats.totalTokens.toLocaleString()}</div>
<div class="stat-value">${window.api.formatNumber(this.stats.total_tokens)}</div>
<div class="stat-label">Total Tokens</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i>
12% from yesterday
<div class="stat-change">
Lifetime usage
</div>
</div>
</div>
@@ -81,11 +69,10 @@ class OverviewPage {
<i class="fas fa-dollar-sign"></i>
</div>
<div class="stat-content">
<div class="stat-value">$${this.stats.totalCost.toFixed(2)}</div>
<div class="stat-value">${window.api.formatCurrency(this.stats.total_cost)}</div>
<div class="stat-label">Total Cost</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i>
$${this.stats.todayCost.toFixed(2)} today
<div class="stat-change ${this.stats.today_cost > 0 ? 'positive' : ''}">
${this.stats.today_cost > 0 ? `<i class="fas fa-arrow-up"></i> ${window.api.formatCurrency(this.stats.today_cost)} today` : '$0.00 today'}
</div>
</div>
</div>
@@ -95,11 +82,10 @@ class OverviewPage {
<i class="fas fa-users"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.stats.activeClients}</div>
<div class="stat-value">${this.stats.active_clients}</div>
<div class="stat-label">Active Clients</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i>
2 new this week
<div class="stat-change">
Unique callers
</div>
</div>
</div>
@@ -109,11 +95,10 @@ class OverviewPage {
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.stats.errorRate}%</div>
<div class="stat-value">${this.stats.error_rate.toFixed(1)}%</div>
<div class="stat-label">Error Rate</div>
<div class="stat-change negative">
<i class="fas fa-arrow-down"></i>
0.5% improvement
<div class="stat-change ${this.stats.error_rate > 5 ? 'negative' : 'positive'}">
${this.stats.error_rate > 5 ? 'Action required' : 'System healthy'}
</div>
</div>
</div>
@@ -123,11 +108,10 @@ class OverviewPage {
<i class="fas fa-tachometer-alt"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.stats.avgResponseTime}ms</div>
<div class="stat-label">Avg Response Time</div>
<div class="stat-change positive">
<i class="fas fa-arrow-down"></i>
50ms faster
<div class="stat-value">${Math.round(this.stats.avg_response_time)}ms</div>
<div class="stat-label">Avg Latency</div>
<div class="stat-change">
Across all providers
</div>
</div>
</div>
@@ -135,31 +119,30 @@ class OverviewPage {
}
async loadCharts() {
await this.loadRequestsChart();
await this.loadProvidersChart();
await this.loadSystemHealth();
await Promise.all([
this.loadRequestsChart(),
this.loadProvidersChart(),
this.loadSystemHealth()
]);
}
async loadRequestsChart() {
try {
// Generate demo data for requests chart
const data = window.chartManager.generateDemoTimeSeries(24, 1);
data.datasets[0].label = 'Requests per hour';
data.datasets[0].fill = true;
const data = await window.api.get('/usage/time-series');
// Create chart
this.charts.requests = window.chartManager.createLineChart('requests-chart', data, {
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `Requests: ${context.parsed.y}`;
}
}
const chartData = {
labels: data.map(item => item.hour),
datasets: [
{
label: 'Requests',
data: data.map(item => item.requests),
color: '#3b82f6',
fill: true
}
}
});
]
};
this.charts.requests = window.chartManager.createLineChart('requests-chart', chartData);
} catch (error) {
console.error('Error loading requests chart:', error);
}
@@ -167,26 +150,15 @@ class OverviewPage {
async loadProvidersChart() {
try {
const data = {
labels: ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'],
data: [45, 25, 20, 10],
colors: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6']
const data = await window.api.get('/usage/providers');
const chartData = {
labels: data.map(item => item.provider),
data: data.map(item => item.requests),
colors: data.map((_, i) => window.chartManager.defaultColors[i % window.chartManager.defaultColors.length])
};
this.charts.providers = window.chartManager.createDoughnutChart('providers-chart', data, {
plugins: {
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
return `${label}: ${value}% of requests`;
}
}
}
}
});
this.charts.providers = window.chartManager.createDoughnutChart('providers-chart', chartData);
} catch (error) {
console.error('Error loading providers chart:', error);
}
@@ -196,117 +168,30 @@ class OverviewPage {
const container = document.getElementById('system-health');
if (!container) return;
const healthData = [
{ label: 'API Server', status: 'online', value: 100 },
{ label: 'Database', status: 'online', value: 95 },
{ label: 'OpenAI', status: 'online', value: 100 },
{ label: 'Gemini', status: 'online', value: 100 },
{ label: 'DeepSeek', status: 'warning', value: 85 },
{ label: 'Grok', status: 'offline', value: 0 }
];
container.innerHTML = healthData.map(item => `
<div class="health-item">
<div class="health-label">
<span class="health-status status-badge ${item.status}">
<i class="fas fa-circle"></i>
${item.status}
</span>
<span class="health-name">${item.label}</span>
</div>
<div class="health-progress">
<div class="progress-bar">
<div class="progress-fill ${item.status}" style="width: ${item.value}%"></div>
try {
const data = await window.api.get('/system/health');
const components = data.components;
container.innerHTML = Object.entries(components).map(([name, status]) => `
<div class="health-item">
<div class="health-label">
<span class="status-badge ${status === 'online' ? 'online' : 'warning'}">
<i class="fas fa-circle"></i>
${status}
</span>
<span class="health-name">${name.toUpperCase()}</span>
</div>
<span class="health-value">${item.value}%</span>
</div>
</div>
`).join('');
// Add CSS for progress bars
this.addHealthStyles();
}
addHealthStyles() {
const style = document.createElement('style');
style.textContent = `
.health-item {
margin-bottom: 1rem;
}
.health-label {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.health-name {
font-size: 0.875rem;
color: var(--text-primary);
}
.health-progress {
display: flex;
align-items: center;
gap: 0.5rem;
}
.progress-bar {
flex: 1;
height: 6px;
background-color: var(--bg-secondary);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-fill.online {
background-color: var(--success);
}
.progress-fill.warning {
background-color: var(--warning);
}
.progress-fill.offline {
background-color: var(--danger);
}
.health-value {
font-size: 0.75rem;
color: var(--text-secondary);
min-width: 40px;
text-align: right;
}
`;
document.head.appendChild(style);
`).join('');
} catch (error) {
console.error('Error loading health:', error);
}
}
async loadRecentRequests() {
try {
// In a real app, this would fetch from /api/requests/recent
// For now, use mock data
const requests = [
{ time: '14:32:15', client: 'client-1', provider: 'OpenAI', model: 'gpt-4', tokens: 1250, status: 'success' },
{ time: '14:30:45', client: 'client-2', provider: 'Gemini', model: 'gemini-pro', tokens: 890, status: 'success' },
{ time: '14:28:12', client: 'client-3', provider: 'DeepSeek', model: 'deepseek-chat', tokens: 1560, status: 'error' },
{ time: '14:25:33', client: 'client-1', provider: 'OpenAI', model: 'gpt-3.5-turbo', tokens: 540, status: 'success' },
{ time: '14:22:18', client: 'client-4', provider: 'Grok', model: 'grok-beta', tokens: 720, status: 'success' },
{ time: '14:20:05', client: 'client-2', provider: 'Gemini', model: 'gemini-pro-vision', tokens: 1120, status: 'success' },
{ time: '14:18:47', client: 'client-5', provider: 'OpenAI', model: 'gpt-4', tokens: 980, status: 'warning' },
{ time: '14:15:22', client: 'client-3', provider: 'DeepSeek', model: 'deepseek-coder', tokens: 1340, status: 'success' },
{ time: '14:12:10', client: 'client-1', provider: 'OpenAI', model: 'gpt-3.5-turbo', tokens: 610, status: 'success' },
{ time: '14:10:05', client: 'client-6', provider: 'Gemini', model: 'gemini-pro', tokens: 830, status: 'success' }
];
this.renderRecentRequests(requests);
const requests = await window.api.get('/system/logs');
this.renderRecentRequests(requests.slice(0, 10)); // Just show top 10 on overview
} catch (error) {
console.error('Error loading recent requests:', error);
}
@@ -316,18 +201,22 @@ class OverviewPage {
const tableBody = document.querySelector('#recent-requests tbody');
if (!tableBody) return;
if (requests.length === 0) {
tableBody.innerHTML = '<tr><td colspan="6" class="text-center">No recent requests</td></tr>';
return;
}
tableBody.innerHTML = requests.map(request => {
const statusClass = request.status === 'success' ? 'success' :
request.status === 'error' ? 'danger' : 'warning';
const statusIcon = request.status === 'success' ? 'check-circle' :
request.status === 'error' ? 'exclamation-circle' : 'exclamation-triangle';
const statusClass = request.status === 'success' ? 'success' : 'danger';
const statusIcon = request.status === 'success' ? 'check-circle' : 'exclamation-circle';
const time = luxon.DateTime.fromISO(request.timestamp).toFormat('HH:mm:ss');
return `
<tr>
<td>${request.time}</td>
<td>${request.client}</td>
<td>${time}</td>
<td><span class="badge-client">${request.client_id}</span></td>
<td>${request.provider}</td>
<td>${request.model}</td>
<td><code class="code-sm">${request.model}</code></td>
<td>${request.tokens.toLocaleString()}</td>
<td>
<span class="status-badge ${statusClass}">
@@ -345,23 +234,17 @@ class OverviewPage {
const periodButtons = document.querySelectorAll('.chart-control-btn[data-period]');
periodButtons.forEach(button => {
button.addEventListener('click', () => {
// Update active state
periodButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Update chart based on period
this.updateRequestsChart(button.dataset.period);
this.loadRequestsChart(); // In real app, pass period to API
});
});
// Refresh button for recent requests
const refreshBtn = document.querySelector('#recent-requests .card-action-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.loadRecentRequests();
if (window.authManager) {
window.authManager.showToast('Recent requests refreshed', 'success');
}
window.authManager.showToast('Recent requests refreshed', 'success');
});
}
}
@@ -369,145 +252,21 @@ class OverviewPage {
setupWebSocketSubscriptions() {
if (!window.wsManager) return;
// Subscribe to request updates
window.wsManager.subscribe('requests', (request) => {
this.handleNewRequest(request);
window.wsManager.subscribe('requests', (event) => {
// Hot-reload stats and table when a new request comes in
this.loadStats();
this.loadRecentRequests();
});
// Subscribe to metric updates
window.wsManager.subscribe('metrics', (metric) => {
this.handleNewMetric(metric);
});
}
handleNewRequest(request) {
// Update total requests counter
if (this.stats) {
this.stats.totalRequests++;
this.stats.todayRequests++;
// Update tokens if available
if (request.tokens) {
this.stats.totalTokens += request.tokens;
}
// Re-render stats
this.renderStats();
}
// Add to recent requests table
this.addToRecentRequests(request);
}
addToRecentRequests(request) {
const tableBody = document.querySelector('#recent-requests tbody');
if (!tableBody) return;
const time = new Date(request.timestamp || Date.now()).toLocaleTimeString();
const statusClass = request.status === 'success' ? 'success' :
request.status === 'error' ? 'danger' : 'warning';
const statusIcon = request.status === 'success' ? 'check-circle' :
request.status === 'error' ? 'exclamation-circle' : 'exclamation-triangle';
const row = document.createElement('tr');
row.innerHTML = `
<td>${time}</td>
<td>${request.client_id || 'Unknown'}</td>
<td>${request.provider || 'Unknown'}</td>
<td>${request.model || 'Unknown'}</td>
<td>${request.tokens || 0}</td>
<td>
<span class="status-badge ${statusClass}">
<i class="fas fa-${statusIcon}"></i>
${request.status || 'unknown'}
</span>
</td>
`;
// Add to top of table
tableBody.insertBefore(row, tableBody.firstChild);
// Limit to 50 rows
const rows = tableBody.querySelectorAll('tr');
if (rows.length > 50) {
tableBody.removeChild(rows[rows.length - 1]);
}
}
handleNewMetric(metric) {
// Update charts with new metric data
if (metric.type === 'requests' && this.charts.requests) {
this.updateRequestsChartData(metric);
}
// Update system health if needed
if (metric.type === 'system_health') {
this.updateSystemHealth(metric);
}
}
updateRequestsChart(period) {
// In a real app, this would fetch new data based on period
// For now, just update with demo data
let hours = 24;
if (period === '7d') hours = 24 * 7;
if (period === '30d') hours = 24 * 30;
const data = window.chartManager.generateDemoTimeSeries(hours, 1);
data.datasets[0].label = 'Requests';
data.datasets[0].fill = true;
window.chartManager.updateChartData('requests-chart', data);
}
updateRequestsChartData(metric) {
// Add new data point to the chart
if (this.charts.requests && metric.value !== undefined) {
window.chartManager.addDataPoint('requests-chart', metric.value);
}
}
updateSystemHealth(metric) {
// Update system health indicators
const container = document.getElementById('system-health');
if (!container || !metric.data) return;
// This would update specific health indicators based on metric data
// Implementation depends on metric structure
}
showError(message) {
const container = document.getElementById('overview-stats');
if (container) {
container.innerHTML = `
<div class="error-message" style="grid-column: 1 / -1;">
<i class="fas fa-exclamation-circle"></i>
<span>${message}</span>
</div>
`;
}
}
refresh() {
this.loadStats();
this.loadRecentRequests();
// Refresh charts
if (this.charts.requests) {
this.charts.requests.update();
}
if (this.charts.providers) {
this.charts.providers.update();
container.innerHTML = `<div class="error-message">${message}</div>`;
}
}
}
// Initialize overview page when needed
window.initOverview = async () => {
window.overviewPage = new OverviewPage();
};
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = OverviewPage;
}