Files
GopherGate/static/js/pages/overview.js
2026-02-26 11:51:36 -05:00

513 lines
18 KiB
JavaScript

// Overview Page Module
class OverviewPage {
constructor() {
this.stats = null;
this.charts = {};
this.init();
}
async init() {
// Load data
await this.loadStats();
await this.loadCharts();
await this.loadRecentRequests();
// Setup event listeners
this.setupEventListeners();
// Subscribe to WebSocket updates
this.setupWebSocketSubscriptions();
}
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
};
this.renderStats();
} catch (error) {
console.error('Error loading stats:', error);
this.showError('Failed to load statistics');
}
}
renderStats() {
const container = document.getElementById('overview-stats');
if (!container) return;
container.innerHTML = `
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-exchange-alt"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.stats.totalRequests.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>
</div>
</div>
<div class="stat-card">
<div class="stat-icon success">
<i class="fas fa-coins"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.stats.totalTokens.toLocaleString()}</div>
<div class="stat-label">Total Tokens</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i>
12% from yesterday
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon warning">
<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-label">Total Cost</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i>
$${this.stats.todayCost.toFixed(2)} today
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon danger">
<i class="fas fa-users"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.stats.activeClients}</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>
</div>
</div>
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.stats.errorRate}%</div>
<div class="stat-label">Error Rate</div>
<div class="stat-change negative">
<i class="fas fa-arrow-down"></i>
0.5% improvement
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon success">
<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>
</div>
</div>
`;
}
async loadCharts() {
await this.loadRequestsChart();
await this.loadProvidersChart();
await 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;
// Create chart
this.charts.requests = window.chartManager.createLineChart('requests-chart', data, {
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `Requests: ${context.parsed.y}`;
}
}
}
}
});
} catch (error) {
console.error('Error loading requests chart:', error);
}
}
async loadProvidersChart() {
try {
const data = {
labels: ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'],
data: [45, 25, 20, 10],
colors: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6']
};
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`;
}
}
}
}
});
} catch (error) {
console.error('Error loading providers chart:', error);
}
}
async loadSystemHealth() {
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>
</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);
}
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);
} catch (error) {
console.error('Error loading recent requests:', error);
}
}
renderRecentRequests(requests) {
const tableBody = document.querySelector('#recent-requests tbody');
if (!tableBody) 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';
return `
<tr>
<td>${request.time}</td>
<td>${request.client}</td>
<td>${request.provider}</td>
<td>${request.model}</td>
<td>${request.tokens.toLocaleString()}</td>
<td>
<span class="status-badge ${statusClass}">
<i class="fas fa-${statusIcon}"></i>
${request.status}
</span>
</td>
</tr>
`;
}).join('');
}
setupEventListeners() {
// Period buttons for requests chart
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);
});
});
// 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');
}
});
}
}
setupWebSocketSubscriptions() {
if (!window.wsManager) return;
// Subscribe to request updates
window.wsManager.subscribe('requests', (request) => {
this.handleNewRequest(request);
});
// 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();
}
}
}
// 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;
}