- overview.js: fix time-series chart crash (data is {series:[...]}, not array; field is 'time' not 'hour')
- monitoring.js: use fallback field names (total_tokens/tokens, duration_ms/duration) for WebSocket vs API compat
- monitoring.js: disable localhost demo data injection that mixed fake data with real
- websocket.js: fix duplicate condition and field name mismatches in dead-code handlers
- logging/mod.rs: add info! logs for successful DB insert and broadcast count for diagnostics
590 lines
19 KiB
JavaScript
590 lines
19 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;
|
|
|
|
const metrics = [
|
|
{ label: 'CPU Usage', value: '24%', trend: 'down', color: 'success' },
|
|
{ label: 'Memory Usage', value: '1.8 GB', trend: 'stable', color: 'warning' },
|
|
{ label: 'Disk I/O', value: '45 MB/s', trend: 'up', color: 'primary' },
|
|
{ label: 'Network', value: '125 KB/s', trend: 'up', color: 'info' },
|
|
{ label: 'Active Connections', value: '42', trend: 'stable', color: 'success' },
|
|
{ label: 'Queue Length', value: '3', trend: 'down', color: 'success' }
|
|
];
|
|
|
|
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('');
|
|
|
|
// Add CSS for metrics
|
|
this.addMetricStyles();
|
|
}
|
|
|
|
addMetricStyles() {
|
|
const style = document.createElement('style');
|
|
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);
|
|
}
|
|
|
|
.monitoring-stream {
|
|
height: 300px;
|
|
overflow-y: auto;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius-sm);
|
|
padding: 0.5rem;
|
|
background-color: var(--bg-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;
|
|
}
|
|
|
|
.log-stream {
|
|
height: 400px;
|
|
overflow-y: auto;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius-sm);
|
|
padding: 0.5rem;
|
|
background-color: var(--bg-secondary);
|
|
font-family: monospace;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.log-entry {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.25rem 0.5rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.log-entry:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.log-time {
|
|
color: var(--text-light);
|
|
min-width: 80px;
|
|
}
|
|
|
|
.log-level {
|
|
width: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.log-info .log-level {
|
|
color: var(--info);
|
|
}
|
|
|
|
.log-warn .log-level {
|
|
color: var(--warning);
|
|
}
|
|
|
|
.log-error .log-level {
|
|
color: var(--danger);
|
|
}
|
|
|
|
.log-debug .log-level {
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.log-message {
|
|
flex: 1;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
async loadCharts() {
|
|
await this.loadResponseTimeChart();
|
|
await this.loadErrorRateChart();
|
|
await this.loadRateLimitChart();
|
|
}
|
|
|
|
async loadResponseTimeChart() {
|
|
try {
|
|
// Generate demo data for response time
|
|
const labels = Array.from({ length: 20 }, (_, i) => `${i + 1}m`);
|
|
const data = {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Response Time (ms)',
|
|
data: labels.map(() => Math.floor(Math.random() * 200) + 300),
|
|
color: '#3b82f6',
|
|
fill: true
|
|
}]
|
|
};
|
|
|
|
window.chartManager.createLineChart('response-time-chart', data, {
|
|
scales: {
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'Milliseconds'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading response time chart:', error);
|
|
}
|
|
}
|
|
|
|
async loadErrorRateChart() {
|
|
try {
|
|
const labels = Array.from({ length: 20 }, (_, i) => `${i + 1}m`);
|
|
const data = {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Error Rate (%)',
|
|
data: labels.map(() => Math.random() * 5),
|
|
color: '#ef4444',
|
|
fill: true
|
|
}]
|
|
};
|
|
|
|
window.chartManager.createLineChart('error-rate-chart', data, {
|
|
scales: {
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'Percentage'
|
|
},
|
|
ticks: {
|
|
callback: function(value) {
|
|
return value + '%';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading error rate chart:', error);
|
|
}
|
|
}
|
|
|
|
async loadRateLimitChart() {
|
|
try {
|
|
const labels = ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'];
|
|
const data = {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Rate Limit Usage',
|
|
data: [65, 45, 78, 34, 60],
|
|
color: '#10b981'
|
|
}]
|
|
};
|
|
|
|
window.chartManager.createBarChart('rate-limit-chart', data, {
|
|
scales: {
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'Percentage'
|
|
},
|
|
ticks: {
|
|
callback: function(value) {
|
|
return value + '%';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading rate limit chart:', error);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// Update charts with new metric data
|
|
if (metric.type === 'response_time' && window.chartManager.charts.has('response-time-chart')) {
|
|
this.updateResponseTimeChart(metric.value);
|
|
}
|
|
|
|
if (metric.type === 'error_rate' && window.chartManager.charts.has('error-rate-chart')) {
|
|
this.updateErrorRateChart(metric.value);
|
|
}
|
|
}
|
|
|
|
updateResponseTimeChart(value) {
|
|
window.chartManager.addDataPoint('response-time-chart', value);
|
|
}
|
|
|
|
updateErrorRateChart(value) {
|
|
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-4', 'gpt-3.5-turbo', 'gemini-pro', 'deepseek-chat', 'grok-beta'];
|
|
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;
|
|
} |