// WebSocket client management let reconnectTimeout = null; function startPollingFallback() { if (window.wsPollingInterval) return; window.wsPollingInterval = setInterval(() => { if (window.activeTab === 'host' && !window.wsConnected) { if (window.genericFetch) { window.genericFetch('host-servers', true); window.genericFetch('host-clients', true); } } }, 5000); } function stopPollingFallback() { if (window.wsPollingInterval) { clearInterval(window.wsPollingInterval); window.wsPollingInterval = null; } } function connectWebSocket() { // Close existing connection if any if (window.ws) { try { window.ws.onopen = null; window.ws.onclose = null; window.ws.onerror = null; window.ws.onmessage = null; if (window.ws.readyState === WebSocket.OPEN || window.ws.readyState === WebSocket.CONNECTING) { window.ws.close(); } } catch (err) { console.error('Error closing existing WebSocket:', err); } } // Clear any pending reconnect timeout if (reconnectTimeout) { clearTimeout(reconnectTimeout); reconnectTimeout = null; } window.ws = new WebSocket('wss://' + location.host + '/ws'); window.ws.onopen = () => { console.log('WebSocket connected'); window.wsConnected = true; window.wsReconnectAttempts = 0; stopPollingFallback(); if (window.updateStatus) window.updateStatus(); if (window.activeTab === 'host' && window.genericFetch) { window.genericFetch('host-servers', true); window.genericFetch('host-clients', true); } }; window.ws.onclose = () => { window.wsConnected = false; if (reconnectTimeout) { clearTimeout(reconnectTimeout); reconnectTimeout = null; return; } const delay = Math.min(1000 * Math.pow(2, window.wsReconnectAttempts), 30000); window.wsReconnectAttempts++; reconnectTimeout = setTimeout(connectWebSocket, delay); if (window.activeTab === 'host') { startPollingFallback(); } }; window.ws.onerror = (err) => { console.error('WebSocket error:', err); window.wsConnected = false; if (window.activeTab === 'host') { startPollingFallback(); } }; window.ws.onmessage = (e) => { const data = JSON.parse(e.data); if (data.type === 'update-holesail-clients') { if (window.genericFetch) { window.genericFetch('host-clients', window.activeTab === 'host').then(() => { let updated = false; for (const pendingId of [...window.pendingClientRestarts]) { const item = window.holesailClientsData?.find(i => i.id === pendingId); if (item && item.info.state === 'running') { if (window.showNotification) window.showNotification('Holesail client restarted successfully'); window.pendingClientRestarts.delete(pendingId); updated = true; } } for (const pendingId of [...window.pendingClientDeletions]) { const item = window.holesailClientsData?.find(i => i.id === pendingId); if (!item) { if (window.showNotification) window.showNotification('Holesail client deleted successfully'); window.pendingClientDeletions.delete(pendingId); updated = true; } } if (updated && window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-clients'); } }); } return; } if (data.type === 'update-holesail') { if (window.genericFetch) { window.genericFetch('host-servers', window.activeTab === 'host').then(() => { let updated = false; for (const pendingId of [...window.pendingServerRestarts]) { const item = window.holesailServersData?.find(i => i.id === pendingId); if (item && item.info.state === 'running') { if (window.showNotification) window.showNotification('Holesail server restarted successfully'); window.pendingServerRestarts.delete(pendingId); updated = true; } } for (const pendingId of [...window.pendingServerDeletions]) { const item = window.holesailServersData?.find(i => i.id === pendingId); if (!item) { if (window.showNotification) window.showNotification('Holesail server deleted successfully'); window.pendingServerDeletions.delete(pendingId); updated = true; } } if (updated && window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-servers'); } }); } return; } if (data.type === 'update-stats' && window.activeTab === 'stats') { if (window.renderStats) window.renderStats(); return; } if (data.type === 'update-settings' && window.activeTab === 'settings') { if (window.fetchSubnets) window.fetchSubnets(); if (window.genericFetch) window.genericFetch('settings', true); return; } if (window.updateMap && window.updateMap[data.type]) { if (typeof window.updateMap[data.type] === 'function') { window.updateMap[data.type](); } else { window.updateMap[data.type].forEach(tab => { // Always fetch data in background, but only render if tab is active if (window.genericFetch) { window.genericFetch(tab, window.activeTab === 'host' || window.activeTab === tab); } }); } } else if (data.type === 'log') { window.logBuffer.push(`[${data.level.toUpperCase()}] ${data.message}`); if (window.logBuffer.length > window.maxLogLines) window.logBuffer.shift(); if (window.activeTab === 'logs' && window.term) { window.term.writeln(`[${data.level.toUpperCase()}] ${data.message}`); } } else if (data.type === 'holesail-log') { let buffer = window.holesailLogBuffers.get(data.id) || []; buffer.push(`[${data.level.toUpperCase()}] ${data.message}`); if (buffer.length > window.maxLogLines) buffer.shift(); window.holesailLogBuffers.set(data.id, buffer); if (window.currentOpenHolesailId === data.id && window.holesailTerm) { window.holesailTerm.writeln(`[${data.level.toUpperCase()}] ${data.message}`); } } }; } function cleanupWebSocket() { if (reconnectTimeout) { clearTimeout(reconnectTimeout); reconnectTimeout = null; } if (window.ws) { try { window.ws.onopen = null; window.ws.onclose = null; window.ws.onerror = null; window.ws.onmessage = null; if (window.ws.readyState === WebSocket.OPEN || window.ws.readyState === WebSocket.CONNECTING) { window.ws.close(); } } catch (err) { console.error('Error closing WebSocket:', err); } window.ws = null; } window.wsConnected = false; } async function updateStatus() { try { const res = await fetch('/api/status'); if (!res.ok) { throw new Error(await res.text()); } const data = await res.json(); let text; let color = 'bg-blue-600'; if (data.isMaster) { text = `This is Master • Peers: ${data.peersCount}`; } else { if (data.isConnected) { text = `Connected to Master • Peers: ${data.peersCount}`; color = 'bg-blue-500'; } else { if (data.peersCount > 0) { text = 'Requesting access...'; color = 'bg-blue-300'; } else { text = 'Searching for peers...'; color = 'bg-blue-300'; } } } if (!window.wsConnected) { text += ' (Polling)'; color = 'bg-yellow-500'; } const indicator = document.getElementById('status-indicator'); if (indicator) { indicator.textContent = text; indicator.className = `fixed top-4 right-4 px-4 py-2 ${color} text-white rounded-lg shadow-md`; } } catch (err) { console.error('Failed to fetch status:', err); const indicator = document.getElementById('status-indicator'); if (indicator) { indicator.textContent = 'Status unknown'; indicator.className = 'fixed top-4 right-4 px-4 py-2 bg-blue-900 text-white rounded-lg shadow-md'; } } } function startStatusUpdates() { if (window.statusUpdateInterval) { clearInterval(window.statusUpdateInterval); } window.statusUpdateInterval = setInterval(updateStatus, 5000); } function stopStatusUpdates() { if (window.statusUpdateInterval) { clearInterval(window.statusUpdateInterval); window.statusUpdateInterval = null; } } // Make functions globally accessible window.connectWebSocket = connectWebSocket; window.startPollingFallback = startPollingFallback; window.stopPollingFallback = stopPollingFallback; window.cleanupWebSocket = cleanupWebSocket; window.updateStatus = updateStatus; window.startStatusUpdates = startStatusUpdates; window.stopStatusUpdates = stopStatusUpdates; // Initialize WebSocket connection if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', connectWebSocket); } else { connectWebSocket(); }