feat: live updates

This commit is contained in:
2025-10-14 20:51:48 +02:00
parent 6db56e470c
commit 25911a183c
9 changed files with 825 additions and 52 deletions

View File

@@ -123,4 +123,166 @@ class ApiClient {
}
// Global API client instance
window.apiClient = new ApiClient();
window.apiClient = new ApiClient();
// WebSocket Client for real-time updates
class WebSocketClient {
constructor() {
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000; // Start with 1 second
this.listeners = new Map();
this.isConnected = false;
// Auto-detect WebSocket URL based on current location
const currentHost = window.location.hostname;
const currentPort = window.location.port;
// Use ws:// for HTTP and wss:// for HTTPS
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
if (currentHost === 'localhost' || currentHost === '127.0.0.1') {
this.wsUrl = `${wsProtocol}//localhost:3001`;
} else {
this.wsUrl = `${wsProtocol}//${currentHost}:3001`;
}
logger.debug('WebSocket Client initialized with URL:', this.wsUrl);
this.connect();
}
connect() {
try {
this.ws = new WebSocket(this.wsUrl);
this.setupEventListeners();
} catch (error) {
logger.error('Failed to create WebSocket connection:', error);
this.scheduleReconnect();
}
}
setupEventListeners() {
this.ws.onopen = () => {
logger.debug('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
// Notify listeners of connection
this.emit('connected');
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
logger.debug('WebSocket message received:', data);
logger.debug('WebSocket message type:', data.type);
this.emit('message', data);
this.handleMessage(data);
} catch (error) {
logger.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onclose = (event) => {
logger.debug('WebSocket disconnected:', event.code, event.reason);
this.isConnected = false;
this.emit('disconnected');
if (event.code !== 1000) { // Not a normal closure
this.scheduleReconnect();
}
};
this.ws.onerror = (error) => {
logger.error('WebSocket error:', error);
this.emit('error', error);
};
}
handleMessage(data) {
switch (data.type) {
case 'cluster_update':
this.emit('clusterUpdate', data);
break;
case 'node_discovery':
this.emit('nodeDiscovery', data);
break;
default:
logger.debug('Unknown WebSocket message type:', data.type);
}
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
logger.error('Max reconnection attempts reached');
this.emit('maxReconnectAttemptsReached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); // Exponential backoff
logger.debug(`Scheduling WebSocket reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
setTimeout(() => {
this.connect();
}, delay);
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
off(event, callback) {
if (this.listeners.has(event)) {
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
emit(event, ...args) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => {
try {
callback(...args);
} catch (error) {
logger.error('Error in WebSocket event listener:', error);
}
});
}
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
logger.warn('WebSocket not connected, cannot send data');
}
}
disconnect() {
if (this.ws) {
this.ws.close(1000, 'Client disconnect');
}
}
getConnectionStatus() {
return {
connected: this.isConnected,
reconnectAttempts: this.reconnectAttempts,
maxReconnectAttempts: this.maxReconnectAttempts,
url: this.wsUrl
};
}
}
// Global WebSocket client instance
window.wsClient = new WebSocketClient();