343 lines
13 KiB
JavaScript
343 lines
13 KiB
JavaScript
// Overview Page Module
|
|
|
|
class OverviewPage {
|
|
constructor() {
|
|
this.stats = null;
|
|
this.charts = {};
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
// Load data
|
|
await Promise.all([
|
|
this.loadStats(),
|
|
this.loadCharts(),
|
|
this.loadRecentRequests()
|
|
]);
|
|
|
|
// Setup event listeners
|
|
this.setupEventListeners();
|
|
|
|
// Subscribe to WebSocket updates
|
|
this.setupWebSocketSubscriptions();
|
|
}
|
|
|
|
async loadStats() {
|
|
try {
|
|
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');
|
|
}
|
|
}
|
|
|
|
renderStats() {
|
|
const container = document.getElementById('overview-stats');
|
|
if (!container || !this.stats) 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.total_requests.toLocaleString()}</div>
|
|
<div class="stat-label">Total Requests</div>
|
|
<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>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon success">
|
|
<i class="fas fa-coins"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-value">${window.api.formatNumber(this.stats.total_tokens)}</div>
|
|
<div class="stat-label">Total Tokens</div>
|
|
<div class="stat-change">
|
|
Lifetime usage
|
|
</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">${window.api.formatCurrency(this.stats.total_cost)}</div>
|
|
<div class="stat-label">Total Cost</div>
|
|
<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>
|
|
|
|
<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.active_clients}</div>
|
|
<div class="stat-label">Active Clients</div>
|
|
<div class="stat-change">
|
|
Unique callers
|
|
</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.error_rate.toFixed(1)}%</div>
|
|
<div class="stat-label">Error Rate</div>
|
|
<div class="stat-change ${this.stats.error_rate > 5 ? 'negative' : 'positive'}">
|
|
${this.stats.error_rate > 5 ? 'Action required' : 'System healthy'}
|
|
</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">${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>
|
|
`;
|
|
}
|
|
|
|
async loadCharts() {
|
|
await Promise.all([
|
|
this.loadRequestsChart(),
|
|
this.loadProvidersChart(),
|
|
this.loadSystemHealth()
|
|
]);
|
|
}
|
|
|
|
async loadRequestsChart() {
|
|
try {
|
|
const cm = window.chartManager || await window.waitForChartManager();
|
|
if (!cm) { this.showEmptyChart('requests-chart', 'Chart system unavailable'); return; }
|
|
|
|
const data = await window.api.get('/usage/time-series');
|
|
const series = data.series || [];
|
|
|
|
if (series.length === 0) {
|
|
this.showEmptyChart('requests-chart', 'No request data in the last 24 hours');
|
|
return;
|
|
}
|
|
|
|
const chartData = {
|
|
labels: series.map(item => item.time),
|
|
datasets: [
|
|
{
|
|
label: 'Requests',
|
|
data: series.map(item => item.requests),
|
|
color: '#3b82f6',
|
|
fill: true
|
|
}
|
|
]
|
|
};
|
|
|
|
this.charts.requests = cm.createLineChart('requests-chart', chartData);
|
|
} catch (error) {
|
|
console.error('Error loading requests chart:', error);
|
|
this.showEmptyChart('requests-chart', 'Failed to load request data');
|
|
}
|
|
}
|
|
|
|
async loadProvidersChart() {
|
|
try {
|
|
const cm = window.chartManager || await window.waitForChartManager();
|
|
if (!cm) { this.showEmptyChart('providers-chart', 'Chart system unavailable'); return; }
|
|
|
|
const data = await window.api.get('/usage/providers');
|
|
|
|
if (!data || data.length === 0) {
|
|
this.showEmptyChart('providers-chart', 'No provider usage data yet');
|
|
return;
|
|
}
|
|
|
|
const chartData = {
|
|
labels: data.map(item => item.provider),
|
|
data: data.map(item => item.requests),
|
|
colors: data.map((_, i) => cm.defaultColors[i % cm.defaultColors.length])
|
|
};
|
|
|
|
this.charts.providers = cm.createDoughnutChart('providers-chart', chartData);
|
|
} catch (error) {
|
|
console.error('Error loading providers chart:', error);
|
|
this.showEmptyChart('providers-chart', 'Failed to load provider 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;
|
|
}
|
|
}
|
|
|
|
async loadSystemHealth() {
|
|
const container = document.getElementById('system-health');
|
|
if (!container) return;
|
|
|
|
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>
|
|
</div>
|
|
`).join('');
|
|
} catch (error) {
|
|
console.error('Error loading health:', error);
|
|
}
|
|
}
|
|
|
|
async loadRecentRequests() {
|
|
try {
|
|
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);
|
|
}
|
|
}
|
|
|
|
renderRecentRequests(requests) {
|
|
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' : 'danger';
|
|
const statusIcon = request.status === 'success' ? 'check-circle' : 'exclamation-circle';
|
|
const time = luxon.DateTime.fromISO(request.timestamp).toFormat('HH:mm:ss');
|
|
|
|
return `
|
|
<tr>
|
|
<td>${time}</td>
|
|
<td><span class="badge-client">${request.client_id}</span></td>
|
|
<td>${request.provider}</td>
|
|
<td><code class="code-sm">${request.model}</code></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', () => {
|
|
periodButtons.forEach(btn => btn.classList.remove('active'));
|
|
button.classList.add('active');
|
|
this.loadRequestsChart(); // In real app, pass period to API
|
|
});
|
|
});
|
|
|
|
const refreshBtn = document.querySelector('#recent-requests .card-action-btn');
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', () => {
|
|
this.loadRecentRequests();
|
|
window.authManager.showToast('Recent requests refreshed', 'success');
|
|
});
|
|
}
|
|
}
|
|
|
|
setupWebSocketSubscriptions() {
|
|
if (!window.wsManager) return;
|
|
|
|
window.wsManager.subscribe('requests', (event) => {
|
|
// Hot-reload stats and table when a new request comes in
|
|
this.loadStats();
|
|
this.addToRecentRequests(event);
|
|
});
|
|
}
|
|
|
|
addToRecentRequests(request) {
|
|
const tableBody = document.querySelector('#recent-requests tbody');
|
|
if (!tableBody) return;
|
|
|
|
// Remove empty message if present
|
|
if (tableBody.querySelector('.text-center')) {
|
|
tableBody.innerHTML = '';
|
|
}
|
|
|
|
const time = luxon.DateTime.fromISO(request.timestamp || new Date().toISOString()).toFormat('HH:mm:ss');
|
|
const statusClass = request.status === 'success' ? 'success' : 'danger';
|
|
const statusIcon = request.status === 'success' ? 'check-circle' : 'exclamation-circle';
|
|
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>${time}</td>
|
|
<td><span class="badge-client">${request.client_id}</span></td>
|
|
<td>${request.provider}</td>
|
|
<td><code class="code-sm">${request.model}</code></td>
|
|
<td>${(request.total_tokens || request.tokens || 0).toLocaleString()}</td>
|
|
<td>
|
|
<span class="status-badge ${statusClass}">
|
|
<i class="fas fa-${statusIcon}"></i>
|
|
${request.status}
|
|
</span>
|
|
</td>
|
|
`;
|
|
|
|
tableBody.insertBefore(row, tableBody.firstChild);
|
|
if (tableBody.children.length > 10) {
|
|
tableBody.lastElementChild.remove();
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
const container = document.getElementById('overview-stats');
|
|
if (container) {
|
|
container.innerHTML = `<div class="error-message">${message}</div>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
window.initOverview = async () => {
|
|
window.overviewPage = new OverviewPage();
|
|
};
|