Files
GopherGate/static/js/websocket.js
hobokenchicken 9380580504
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
fix: resolve dashboard websocket 'disconnected' status
- Fixed status indicator UI mapping in websocket.js and index.html.
- Added missing CSS for connection status indicator and pulse animation.
- Made initial model registry fetch asynchronous to prevent blocking server startup.
- Improved configuration loading to correctly handle LLM_PROXY__SERVER__PORT from environment.
2026-03-19 14:32:34 -04:00

493 lines
16 KiB
JavaScript

// WebSocket Manager for Real-time Updates
class WebSocketManager {
constructor() {
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.isConnected = false;
this.subscribers = new Map();
this.init();
}
init() {
this.connect();
this.setupStatusIndicator();
this.setupAutoReconnect();
}
connect() {
try {
// Determine WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const wsUrl = `${protocol}//${host}/ws`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => this.onOpen();
this.ws.onclose = () => this.onClose();
this.ws.onerror = (error) => this.onError(error);
this.ws.onmessage = (event) => this.onMessage(event);
} catch (error) {
console.error('WebSocket connection error:', error);
this.scheduleReconnect();
}
}
onOpen() {
console.log('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
this.updateStatus('connected');
// Notify subscribers
this.notify('connection', { status: 'connected' });
// Send authentication if needed
if (window.authManager && window.authManager.token) {
this.send({
type: 'auth',
token: window.authManager.token
});
}
// Subscribe to default channels
this.send({
type: 'subscribe',
channels: ['requests', 'metrics', 'logs']
});
}
onClose() {
console.log('WebSocket disconnected');
this.isConnected = false;
this.updateStatus('disconnected');
// Notify subscribers
this.notify('connection', { status: 'disconnected' });
// Schedule reconnection
this.scheduleReconnect();
}
onError(error) {
console.error('WebSocket error:', error);
this.updateStatus('error');
}
onMessage(event) {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
}
handleMessage(data) {
// Handle request events
if (data.type === 'request') {
this.notify('requests', data.payload);
}
// Notify specific channel subscribers
const channel = data.channel || data.type;
if (channel && this.subscribers.has(channel)) {
this.subscribers.get(channel).forEach(callback => {
try {
callback(data.payload);
} catch (error) {
console.error('Error in WebSocket callback:', error);
}
});
}
}
handleRequest(request) {
// Update request counters
this.updateRequestCounters(request);
// Add to recent requests if on overview page
if (window.dashboard && window.dashboard.currentPage === 'overview') {
this.addRecentRequest(request);
}
// Update monitoring stream if on monitoring page
if (window.dashboard && window.dashboard.currentPage === 'monitoring') {
this.addToMonitoringStream(request);
}
}
handleMetric(metric) {
// Update charts with new metric data
this.updateCharts(metric);
// Update system metrics display
this.updateSystemMetrics(metric);
}
handleLog(log) {
// Add to logs table if on logs page
if (window.dashboard && window.dashboard.currentPage === 'logs') {
this.addLogEntry(log);
}
// Add to monitoring logs if on monitoring page
if (window.dashboard && window.dashboard.currentPage === 'monitoring') {
this.addToLogStream(log);
}
}
handleSystem(system) {
// Update system health indicators
this.updateSystemHealth(system);
}
handleError(error) {
console.error('Server error:', error);
// Show error toast
if (window.authManager) {
window.authManager.showToast(error.message || 'Server error', 'error');
}
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
return true;
} else {
console.warn('WebSocket not connected, message not sent:', data);
return false;
}
}
subscribe(channel, callback) {
if (!this.subscribers.has(channel)) {
this.subscribers.set(channel, new Set());
}
this.subscribers.get(channel).add(callback);
// Send subscription to server
this.send({
type: 'subscribe',
channels: [channel]
});
// Return unsubscribe function
return () => this.unsubscribe(channel, callback);
}
unsubscribe(channel, callback) {
if (this.subscribers.has(channel)) {
this.subscribers.get(channel).delete(callback);
// If no more subscribers, unsubscribe from server
if (this.subscribers.get(channel).size === 0) {
this.send({
type: 'unsubscribe',
channels: [channel]
});
}
}
}
notify(channel, data) {
if (this.subscribers.has(channel)) {
this.subscribers.get(channel).forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('Error in notification callback:', error);
}
});
}
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.warn('Max reconnection attempts reached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Scheduling reconnection in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
if (!this.isConnected) {
this.connect();
}
}, delay);
}
setupAutoReconnect() {
// Reconnect when browser comes online
window.addEventListener('online', () => {
if (!this.isConnected) {
console.log('Browser online, attempting to reconnect...');
this.connect();
}
});
// Keepalive ping
setInterval(() => {
if (this.isConnected) {
this.send({ type: 'ping' });
}
}, 30000);
}
setupStatusIndicator() {
// Status indicator is already in the HTML
// This function just ensures it's properly styled
}
updateStatus(status) {
const statusElement = document.getElementById('connection-status');
if (!statusElement) return;
const dot = statusElement.querySelector('.status-dot');
const text = statusElement.querySelector('.status-text');
if (!dot || !text) return;
// Remove all status classes
dot.classList.remove('connected', 'disconnected', 'error', 'connecting');
// Add new status class
dot.classList.add(status);
// Update text
const statusText = {
'connected': 'Connected',
'disconnected': 'Disconnected',
'connecting': 'Connecting...',
'error': 'Connection Error'
};
text.textContent = statusText[status] || status;
}
// Helper methods for updating UI
updateRequestCounters(request) {
// Update request counters in overview stats
const requestCountElement = document.querySelector('[data-stat="total-requests"]');
if (requestCountElement) {
const currentCount = parseInt(requestCountElement.textContent) || 0;
requestCountElement.textContent = currentCount + 1;
}
// Update token counters
const tokenCountElement = document.querySelector('[data-stat="total-tokens"]');
if (tokenCountElement && (request.total_tokens || request.tokens)) {
const currentTokens = parseInt(tokenCountElement.textContent) || 0;
tokenCountElement.textContent = currentTokens + (request.total_tokens || request.tokens);
}
}
addRecentRequest(request) {
const tableBody = document.querySelector('#recent-requests tbody');
if (!tableBody) return;
const row = document.createElement('tr');
// Format time
const time = new Date(request.timestamp || Date.now()).toLocaleTimeString();
// Format status badge
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';
row.innerHTML = `
<td>${time}</td>
<td>${request.client_id || 'Unknown'}</td>
<td>${request.provider || 'Unknown'}</td>
<td>${request.model || 'Unknown'}</td>
<td>${(request.total_tokens || 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]);
}
}
addToMonitoringStream(request) {
const streamElement = document.getElementById('request-stream');
if (!streamElement) return;
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);
// Limit to 20 entries
const entries = streamElement.querySelectorAll('.stream-entry');
if (entries.length > 20) {
streamElement.removeChild(entries[entries.length - 1]);
}
// Add highlight animation
entry.classList.add('highlight');
setTimeout(() => entry.classList.remove('highlight'), 1000);
}
updateCharts(metric) {
// This would update Chart.js charts with new data
// Implementation depends on specific chart setup
}
updateSystemMetrics(metric) {
const metricsElement = document.getElementById('system-metrics');
if (!metricsElement) return;
// Update specific metric displays
// This is a simplified example
}
addLogEntry(log) {
const tableBody = document.querySelector('#logs-table tbody');
if (!tableBody) return;
const row = document.createElement('tr');
// Format time
const time = new Date(log.timestamp || Date.now()).toLocaleString();
// Determine log level class
const levelClass = log.level || 'info';
row.innerHTML = `
<td>${time}</td>
<td>
<span class="status-badge ${levelClass}">
${levelClass.toUpperCase()}
</span>
</td>
<td>${log.source || 'Unknown'}</td>
<td>${log.message || ''}</td>
`;
// Add to top of table
tableBody.insertBefore(row, tableBody.firstChild);
// Limit to 100 rows
const rows = tableBody.querySelectorAll('tr');
if (rows.length > 100) {
tableBody.removeChild(rows[rows.length - 1]);
}
}
addToLogStream(log) {
const logStreamElement = document.getElementById('system-logs');
if (!logStreamElement) return;
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
logStreamElement.insertBefore(entry, logStreamElement.firstChild);
// Limit to 50 entries
const entries = logStreamElement.querySelectorAll('.log-entry');
if (entries.length > 50) {
logStreamElement.removeChild(entries[entries.length - 1]);
}
}
updateSystemHealth(system) {
const healthElement = document.getElementById('system-health');
if (!healthElement) return;
// Update system health indicators
// This is a simplified example
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.isConnected = false;
this.updateStatus('disconnected');
}
reconnect() {
this.disconnect();
this.connect();
}
}
// Initialize WebSocket manager when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.wsManager = new WebSocketManager();
});
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = WebSocketManager;
}