Files
GopherGate/static/js/pages/monitoring.js
hobokenchicken 9207a7231c
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
chore: update all grok-2 references to grok-4
2026-03-25 13:17:06 +00:00

583 lines
20 KiB
JavaScript

// 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 => `
<div class="metric-item">
<div class="metric-label">${metric.label}</div>
<div class="metric-value">${metric.value}</div>
<div class="metric-trend ${metric.trend}">
<i class="fas fa-arrow-${metric.trend === 'up' ? 'up' : metric.trend === 'down' ? 'down' : 'minus'}"></i>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading system metrics:', error);
container.innerHTML = '<div class="metric-item"><div class="metric-label" style="color: var(--danger);">Failed to load metrics</div></div>';
}
// 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 = '<i class="fas fa-play"></i> Resume';
pauseBtn.classList.remove('btn-secondary');
pauseBtn.classList.add('btn-success');
if (window.authManager) {
window.authManager.showToast('Monitoring paused', 'warning');
}
} else {
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> 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 = `
<div class="stream-entry-time">${time}</div>
<div class="stream-entry-icon" style="color: ${color}">
<i class="fas fa-${icon}"></i>
</div>
<div class="stream-entry-content">
<strong>${request.client_id || 'Unknown'}</strong> →
${request.provider || 'Unknown'} (${request.model || 'Unknown'})
<div class="stream-entry-details">
${request.total_tokens || request.tokens || 0} tokens • ${request.duration_ms || request.duration || 0}ms
</div>
</div>
`;
// 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 = `
<div class="log-time">${time}</div>
<div class="log-level">
<i class="fas fa-${icon}"></i>
</div>
<div class="log-message">${log.message || ''}</div>
`;
// 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;
}