// Monitoring Page Module class MonitoringPage { constructor() { this.isPaused = false; this.requestStream = []; this.systemLogs = []; this.init(); } async init() { // Load initial data await this.loadSystemMetrics(); await this.loadCharts(); // Setup event listeners this.setupEventListeners(); // Setup WebSocket subscriptions this.setupWebSocketSubscriptions(); // Start simulated updates for demo this.startDemoUpdates(); } async loadSystemMetrics() { const container = document.getElementById('system-metrics'); if (!container) return; try { const data = await window.api.get('/system/metrics'); const metrics = [ { label: 'CPU Usage', value: `${data.cpu.usage_percent}%`, trend: data.cpu.usage_percent > 80 ? 'up' : data.cpu.usage_percent < 30 ? 'down' : 'stable' }, { label: 'Memory', value: `${(data.memory.used_mb / 1024).toFixed(1)} / ${(data.memory.total_mb / 1024).toFixed(1)} GB`, trend: data.memory.usage_percent > 80 ? 'up' : 'stable' }, { label: 'Disk', value: `${data.disk.used_gb.toFixed(1)} / ${data.disk.total_gb.toFixed(1)} GB`, trend: data.disk.usage_percent > 80 ? 'up' : 'stable' }, { label: 'Process RSS', value: `${data.memory.process_rss_mb} MB`, trend: 'stable' }, { label: 'Load Average', value: data.cpu.load_average.map(v => v.toFixed(2)).join(' / '), trend: data.cpu.load_average[0] > 2 ? 'up' : 'down' }, { label: 'Connections', value: `${data.connections.db_active} DB, ${data.connections.websocket_listeners} WS`, trend: 'stable' }, ]; container.innerHTML = metrics.map(metric => `
${metric.label}
${metric.value}
`).join(''); } catch (error) { console.error('Error loading system metrics:', error); container.innerHTML = '
Failed to load metrics
'; } // Add CSS for metrics this.addMetricStyles(); } addMetricStyles() { // Avoid injecting duplicate styles if (document.getElementById('monitoring-metric-styles')) return; const style = document.createElement('style'); style.id = 'monitoring-metric-styles'; style.textContent = ` .metric-item { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem; border-bottom: 1px solid var(--border-color); } .metric-item:last-child { border-bottom: none; } .metric-label { font-size: 0.875rem; color: var(--text-secondary); } .metric-value { font-weight: 600; color: var(--text-primary); } .metric-trend { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; } .metric-trend.up { background-color: rgba(239, 68, 68, 0.1); color: var(--danger); } .metric-trend.down { background-color: rgba(16, 185, 129, 0.1); color: var(--success); } .metric-trend.stable { background-color: rgba(100, 116, 139, 0.1); color: var(--text-secondary); } .stream-entry { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; border-bottom: 1px solid var(--border-color); transition: background-color 0.2s ease; } .stream-entry:last-child { border-bottom: none; } .stream-entry.highlight { background-color: rgba(37, 99, 235, 0.05); } .stream-entry-time { font-size: 0.75rem; color: var(--text-light); min-width: 60px; } .stream-entry-icon { font-size: 0.875rem; width: 24px; text-align: center; } .stream-entry-content { flex: 1; font-size: 0.875rem; } .stream-entry-details { font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.125rem; } `; document.head.appendChild(style); } async loadCharts() { // Ensure chartManager is available const cm = window.chartManager || await window.waitForChartManager(); if (!cm) { console.warn('chartManager unavailable, skipping monitoring charts'); return; } // Fetch recent logs for chart data try { const logs = await window.api.get('/system/logs'); this.recentLogs = Array.isArray(logs) ? logs : []; } catch (error) { console.error('Error loading logs for charts:', error); this.recentLogs = []; } await this.loadResponseTimeChart(); await this.loadErrorRateChart(); await this.loadRateLimitChart(); } async loadResponseTimeChart() { try { const cm = window.chartManager; if (!cm) return; // Bucket recent logs by minute for latency chart const buckets = this.bucketByMinute(this.recentLogs, 20); const labels = buckets.map(b => b.label); const values = buckets.map(b => { if (b.items.length === 0) return 0; const total = b.items.reduce((sum, r) => sum + (r.duration || 0), 0); return Math.round(total / b.items.length); }); const data = { labels, datasets: [{ label: 'Avg Response Time (ms)', data: values, color: '#83a598', fill: true }] }; cm.createLineChart('response-time-chart', data, { scales: { y: { title: { display: true, text: 'Milliseconds' }, beginAtZero: true } } }); } catch (error) { console.error('Error loading response time chart:', error); } } async loadErrorRateChart() { try { const cm = window.chartManager; if (!cm) return; const buckets = this.bucketByMinute(this.recentLogs, 20); const labels = buckets.map(b => b.label); const values = buckets.map(b => { if (b.items.length === 0) return 0; const errors = b.items.filter(r => r.status === 'error').length; return parseFloat((errors / b.items.length * 100).toFixed(1)); }); const data = { labels, datasets: [{ label: 'Error Rate (%)', data: values, color: '#fb4934', fill: true }] }; cm.createLineChart('error-rate-chart', data, { scales: { y: { title: { display: true, text: 'Percentage' }, beginAtZero: true, ticks: { callback: function(value) { return value + '%'; } } } } }); } catch (error) { console.error('Error loading error rate chart:', error); } } async loadRateLimitChart() { try { const cm = window.chartManager; if (!cm) return; // Show requests-per-client from recent logs const clientCounts = {}; for (const log of this.recentLogs) { const client = log.client_id || 'unknown'; clientCounts[client] = (clientCounts[client] || 0) + 1; } const sorted = Object.entries(clientCounts).sort((a, b) => b[1] - a[1]).slice(0, 8); const labels = sorted.map(([c]) => c.length > 16 ? c.substring(0, 14) + '...' : c); const values = sorted.map(([, v]) => v); const data = { labels: labels.length > 0 ? labels : ['No data'], datasets: [{ label: 'Requests by Client', data: values.length > 0 ? values : [0], color: '#8ec07c' }] }; cm.createBarChart('rate-limit-chart', data, { scales: { y: { title: { display: true, text: 'Request Count' }, beginAtZero: true } } }); } catch (error) { console.error('Error loading rate limit chart:', error); } } /** Bucket log entries into N-minute-wide bins ending at now. */ bucketByMinute(logs, count) { const now = Date.now(); const buckets = Array.from({ length: count }, (_, i) => { const minutesAgo = count - 1 - i; return { label: minutesAgo === 0 ? 'now' : `${minutesAgo}m`, items: [], start: now - (minutesAgo + 1) * 60000, end: now - minutesAgo * 60000 }; }); for (const log of logs) { const ts = new Date(log.timestamp).getTime(); for (const bucket of buckets) { if (ts >= bucket.start && ts < bucket.end) { bucket.items.push(log); break; } } } return buckets; } setupEventListeners() { // Pause/resume monitoring button const pauseBtn = document.getElementById('pause-monitoring'); if (pauseBtn) { pauseBtn.addEventListener('click', () => { this.togglePause(); }); } } setupWebSocketSubscriptions() { if (!window.wsManager) return; // Subscribe to request updates window.wsManager.subscribe('requests', (request) => { if (!this.isPaused) { this.addToRequestStream(request); } }); // Subscribe to log updates window.wsManager.subscribe('logs', (log) => { if (!this.isPaused) { this.addToLogStream(log); } }); // Subscribe to metric updates window.wsManager.subscribe('metrics', (metric) => { if (!this.isPaused) { this.updateCharts(metric); } }); } togglePause() { this.isPaused = !this.isPaused; const pauseBtn = document.getElementById('pause-monitoring'); if (pauseBtn) { if (this.isPaused) { pauseBtn.innerHTML = ' Resume'; pauseBtn.classList.remove('btn-secondary'); pauseBtn.classList.add('btn-success'); if (window.authManager) { window.authManager.showToast('Monitoring paused', 'warning'); } } else { pauseBtn.innerHTML = ' Pause'; pauseBtn.classList.remove('btn-success'); pauseBtn.classList.add('btn-secondary'); if (window.authManager) { window.authManager.showToast('Monitoring resumed', 'success'); } } } } addToRequestStream(request) { const streamElement = document.getElementById('request-stream'); if (!streamElement) return; // Create entry const entry = document.createElement('div'); entry.className = 'stream-entry'; // Format time const time = new Date().toLocaleTimeString(); // Determine icon based on status let icon = 'question-circle'; let color = 'var(--text-secondary)'; if (request.status === 'success') { icon = 'check-circle'; color = 'var(--success)'; } else if (request.status === 'error') { icon = 'exclamation-circle'; color = 'var(--danger)'; } entry.innerHTML = `
${time}
${request.client_id || 'Unknown'} → ${request.provider || 'Unknown'} (${request.model || 'Unknown'})
${request.total_tokens || request.tokens || 0} tokens • ${request.duration_ms || request.duration || 0}ms
`; // Add to top of stream streamElement.insertBefore(entry, streamElement.firstChild); // Store in memory (limit to 100) this.requestStream.unshift({ time, request, element: entry }); if (this.requestStream.length > 100) { const oldEntry = this.requestStream.pop(); if (oldEntry.element.parentNode) { oldEntry.element.remove(); } } // Add highlight animation entry.classList.add('highlight'); setTimeout(() => entry.classList.remove('highlight'), 1000); } addToLogStream(log) { const logElement = document.getElementById('system-logs'); if (!logElement) return; // Create entry const entry = document.createElement('div'); entry.className = `log-entry log-${log.level || 'info'}`; // Format time const time = new Date().toLocaleTimeString(); // Determine icon based on level let icon = 'info-circle'; if (log.level === 'error') icon = 'exclamation-circle'; if (log.level === 'warn') icon = 'exclamation-triangle'; if (log.level === 'debug') icon = 'bug'; entry.innerHTML = `
${time}
${log.message || ''}
`; // Add to top of stream logElement.insertBefore(entry, logElement.firstChild); // Store in memory (limit to 100) this.systemLogs.unshift({ time, log, element: entry }); if (this.systemLogs.length > 100) { const oldEntry = this.systemLogs.pop(); if (oldEntry.element.parentNode) { oldEntry.element.remove(); } } } updateCharts(metric) { const cm = window.chartManager; if (!cm) return; // Update charts with new metric data if (metric.type === 'response_time' && cm.charts.has('response-time-chart')) { this.updateResponseTimeChart(metric.value); } if (metric.type === 'error_rate' && cm.charts.has('error-rate-chart')) { this.updateErrorRateChart(metric.value); } } updateResponseTimeChart(value) { if (window.chartManager) window.chartManager.addDataPoint('response-time-chart', value); } updateErrorRateChart(value) { if (window.chartManager) window.chartManager.addDataPoint('error-rate-chart', value); } startDemoUpdates() { // Demo updates disabled — real data comes via WebSocket subscriptions } simulateRequest() { const clients = ['client-1', 'client-2', 'client-3', 'client-4', 'client-5']; const providers = ['OpenAI', 'Gemini', 'DeepSeek', 'Grok']; const models = ['gpt-4o', 'gpt-4o-mini', 'gemini-2.0-flash', 'deepseek-chat', 'grok-4-1-fast-non-reasoning']; const statuses = ['success', 'success', 'success', 'error', 'warning']; // Mostly success const request = { client_id: clients[Math.floor(Math.random() * clients.length)], provider: providers[Math.floor(Math.random() * providers.length)], model: models[Math.floor(Math.random() * models.length)], tokens: Math.floor(Math.random() * 2000) + 100, duration: Math.floor(Math.random() * 1000) + 100, status: statuses[Math.floor(Math.random() * statuses.length)], timestamp: Date.now() }; this.addToRequestStream(request); } simulateLog() { const levels = ['info', 'info', 'info', 'warn', 'error']; const messages = [ 'Request processed successfully', 'Cache hit for model gpt-4', 'Rate limit check passed', 'High latency detected for DeepSeek provider', 'API key validation failed', 'Database connection pool healthy', 'New client registered: client-7', 'Backup completed successfully', 'Memory usage above 80% threshold', 'Provider Grok is offline' ]; const log = { level: levels[Math.floor(Math.random() * levels.length)], message: messages[Math.floor(Math.random() * messages.length)], timestamp: Date.now() }; this.addToLogStream(log); } simulateMetric() { const metricTypes = ['response_time', 'error_rate']; const type = metricTypes[Math.floor(Math.random() * metricTypes.length)]; let value; if (type === 'response_time') { value = Math.floor(Math.random() * 200) + 300; // 300-500ms } else { value = Math.random() * 5; // 0-5% } this.updateCharts({ type, value }); } clearStreams() { const streamElement = document.getElementById('request-stream'); const logElement = document.getElementById('system-logs'); if (streamElement) { streamElement.innerHTML = ''; this.requestStream = []; } if (logElement) { logElement.innerHTML = ''; this.systemLogs = []; } } refresh() { this.loadSystemMetrics(); this.loadCharts(); this.clearStreams(); if (window.authManager) { window.authManager.showToast('Monitoring refreshed', 'success'); } } } // Initialize monitoring page when needed window.initMonitoring = async () => { window.monitoringPage = new MonitoringPage(); }; // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = MonitoringPage; }