/** * WebSocket-Client für Echtzeit-Updates. */ const WS = { socket: null, reconnectDelay: 2000, maxReconnectDelay: 30000, _handlers: {}, _pingInterval: null, connect() { const token = localStorage.getItem('osint_token'); if (!token) return; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const url = `${protocol}//${window.location.host}/api/ws`; try { this.socket = new WebSocket(url); } catch (e) { console.error('WebSocket-Verbindungsfehler:', e); this._scheduleReconnect(); return; } this.socket.onopen = () => { // Token als erste Nachricht senden (nicht in URL) this.socket.send(token); }; this.socket.onmessage = (event) => { if (event.data === 'pong') return; if (event.data === 'authenticated') { console.log('WebSocket verbunden'); this.reconnectDelay = 2000; this._startPing(); return; } try { const msg = JSON.parse(event.data); this._dispatch(msg); } catch (e) { console.error('WebSocket Parse-Fehler:', e); } }; this.socket.onclose = () => { console.log('WebSocket getrennt'); this._stopPing(); this._scheduleReconnect(); }; this.socket.onerror = () => {}; }, disconnect() { this._stopPing(); if (this.socket) { this.socket.close(); this.socket = null; } }, on(type, handler) { if (!this._handlers[type]) { this._handlers[type] = []; } this._handlers[type].push(handler); }, _dispatch(msg) { const handlers = this._handlers[msg.type] || []; handlers.forEach(h => h(msg)); // Globale Handler const allHandlers = this._handlers['*'] || []; allHandlers.forEach(h => h(msg)); }, _startPing() { this._pingInterval = setInterval(() => { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send('ping'); } }, 30000); }, _stopPing() { if (this._pingInterval) { clearInterval(this._pingInterval); this._pingInterval = null; } }, _scheduleReconnect() { setTimeout(() => { if (!this.socket || this.socket.readyState === WebSocket.CLOSED) { this.connect(); } }, this.reconnectDelay); this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, this.maxReconnectDelay); }, };