513 lines
18 KiB
JavaScript
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;
|
|
} |