/** * BridgeSwarm Dashboard - UI logic */ const SETTINGS_KEY = 'bridgeSwarmSettings'; let currentState = null; let settings = {}; function $(id) { return document.getElementById(id); } function log(...args) { console.log('[BridgeSwarm-dashboard]', ...args); } // Format time ago function timeAgo(timestamp) { const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return seconds + 's ago'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return minutes + 'm ago'; const hours = Math.floor(minutes / 60); if (hours < 24) return hours + 'h ago'; return Math.floor(hours / 24) + 'd ago'; } // Format uptime function formatUptime(ms) { const seconds = Math.floor(ms / 1000); if (seconds < 60) return seconds + 's'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return minutes + 'm ' + (seconds % 60) + 's'; const hours = Math.floor(minutes / 60); const mins = minutes % 60; return hours + 'h ' + mins + 'm'; } // Truncate string function truncate(str, len = 16) { if (!str) return ''; return str.length > len ? str.slice(0, len) + '…' : str; } // Fetch state from background async function fetchState() { return new Promise((resolve) => { chrome.runtime.sendMessage( { target: 'bridge-swarm-native', action: 'getState' }, (response) => { if (response && response.ok) { resolve(response.state); } else { resolve(null); } } ); }); } // Load settings function loadSettings() { chrome.storage.local.get(SETTINGS_KEY, (result) => { settings = result[SETTINGS_KEY] || {}; updateSettingsUI(); }); } // Update settings UI function updateSettingsUI() { $('toggleNotify').classList.toggle('active', settings.notifyOnDisconnect === true); $('toggleDebug').classList.toggle('active', settings.debug === true); $('toggleDisableFileUrls')?.classList.toggle('active', settings.disableOnFileUrls === true); $('defaultAppName').value = settings.defaultAppName || 'bridge-swarm'; $('defaultMaxPeers').value = settings.defaultMaxPeers || ''; $('defaultRequestTimeoutMs').value = settings.defaultRequestTimeoutMs || ''; $('readyTimeoutMs').value = settings.readyTimeoutMs || ''; } // Save settings function saveSettings() { settings.notifyOnDisconnect = $('toggleNotify').classList.contains('active'); settings.debug = $('toggleDebug').classList.contains('active'); settings.disableOnFileUrls = $('toggleDisableFileUrls')?.classList.contains('active') || false; settings.defaultAppName = $('defaultAppName').value || 'bridge-swarm'; settings.defaultMaxPeers = parseInt($('defaultMaxPeers').value) || 0; settings.defaultRequestTimeoutMs = parseInt($('defaultRequestTimeoutMs').value) || 0; settings.readyTimeoutMs = parseInt($('readyTimeoutMs').value) || 0; chrome.storage.local.set({ [SETTINGS_KEY]: settings }, () => { log('Settings saved'); }); } // Update dashboard function updateDashboard(state) { currentState = state; // Status banner const banner = $('statusBanner'); const statusText = $('statusText'); if (state.hostConnected) { banner.className = 'status-banner connected'; statusText.textContent = 'Native host connected'; } else { banner.className = 'status-banner disconnected'; statusText.textContent = 'Native host disconnected'; } // Stats $('dashSwarms').textContent = state.stats.totalSwarms; $('dashConnections').textContent = state.stats.totalConnections; $('dashTabs').textContent = state.tabs.length; $('dashUptime').textContent = formatUptime(Date.now() - state.stats.uptime); // Badges $('swarmCount').textContent = state.stats.totalSwarms; $('connCount').textContent = state.stats.totalConnections; $('tabCount').textContent = state.tabs.length; // Recent connections const connections = []; for (const swarm of state.swarms) { for (const conn of swarm.connections) { connections.push(conn); } } const recentConnBody = $('recentConnections'); if (connections.length === 0) { recentConnBody.innerHTML = 'No active connections'; } else { recentConnBody.innerHTML = connections.slice(0, 10).map(conn => ` ${truncate(conn.connId)} ${truncate(conn.swarmId, 12)} ${truncate(conn.peerKey, 12)} ${timeAgo(conn.createdAt)} `).join(''); } } // Update swarms table function updateSwarmsTable(state) { const tbody = $('swarmsTable'); if (state.swarms.length === 0) { tbody.innerHTML = 'No active swarms'; return; } tbody.innerHTML = state.swarms.map(swarm => ` ${truncate(swarm.swarmId, 20)} ${swarm.appName} ${swarm.connections.length} ${swarm.tabCount} `).join(''); } // Update connections table function updateConnectionsTable(state) { const connections = []; for (const swarm of state.swarms) { for (const conn of swarm.connections) { connections.push(conn); } } const tbody = $('connectionsTable'); if (connections.length === 0) { tbody.innerHTML = 'No active connections'; return; } tbody.innerHTML = connections.map(conn => ` ${truncate(conn.connId, 20)} ${truncate(conn.swarmId, 16)} ${truncate(conn.peerKey, 16)} ${timeAgo(conn.createdAt)} `).join(''); } // Update tabs table function updateTabsTable(state) { const tbody = $('tabsTable'); if (state.tabs.length === 0) { tbody.innerHTML = 'No active tabs'; return; } tbody.innerHTML = state.tabs.map(tab => ` ${tab.tabId} ${tab.swarmIds.map(id => `${truncate(id, 12)}`).join(', ') || 'none'} `).join(''); } // Destroy swarm async function destroySwarm(swarmId) { if (!confirm(`Destroy swarm "${swarmId}"? This will disconnect all peers.`)) { return; } chrome.runtime.sendMessage( { target: 'bridge-swarm-native', action: 'destroySwarm', payload: { swarmId } }, (response) => { if (response && response.ok) { log('Swarm destroyed:', swarmId); refresh(); } else { log('Failed to destroy swarm:', response?.error); } } ); } // Make destroySwarm global window.destroySwarm = destroySwarm; // Refresh all data async function refresh() { const state = await fetchState(); if (state) { updateDashboard(state); updateSwarmsTable(state); updateConnectionsTable(state); updateTabsTable(state); } } // Navigation function setupNavigation() { const navItems = document.querySelectorAll('.nav-item'); const pages = document.querySelectorAll('.page'); navItems.forEach(item => { item.addEventListener('click', () => { const page = item.dataset.page; // Update nav navItems.forEach(i => i.classList.remove('active')); item.classList.add('active'); // Update page pages.forEach(p => p.classList.remove('active')); $(`page-${page}`).classList.add('active'); }); }); } // Setup event listeners function setupEvents() { // Refresh button $('refreshBtn').addEventListener('click', refresh); // Toggle switches document.querySelectorAll('.toggle').forEach(toggle => { toggle.addEventListener('click', () => { toggle.classList.toggle('active'); }); }); // Save settings $('btnSaveSettings').addEventListener('click', saveSettings); // Logs functionality let logs = []; let autoScroll = true; // Register as dashboard chrome.runtime.sendMessage( { target: 'bridge-swarm-native', action: 'registerDashboard' }, (response) => { if (response && response.logs) { logs = response.logs; updateLogsDisplay(); } } ); // Listen for log updates chrome.runtime.onMessage.addListener((message) => { if (message.type === 'bridge-swarm-logs' && message.logs) { logs = message.logs; updateLogsDisplay(); } }); function updateLogsDisplay() { const container = $('logsContainer'); if (!container) return; if (logs.length === 0) { container.innerHTML = '
No logs yet
'; return; } container.innerHTML = logs.map(entry => { const time = new Date(entry.timestamp).toLocaleTimeString(); return `
${time} ${escapeHtml(entry.message)}
`; }).join(''); if (autoScroll) { container.scrollTop = container.scrollHeight; } } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // Clear logs button $('btnClearLogs')?.addEventListener('click', () => { logs = []; updateLogsDisplay(); }); // Auto-scroll toggle $('btnAutoScroll')?.addEventListener('click', () => { autoScroll = !autoScroll; $('btnAutoScroll').textContent = 'Auto-scroll: ' + (autoScroll ? 'ON' : 'OFF'); }); // Unregister dashboard on page unload window.addEventListener('beforeunload', () => { chrome.runtime.sendMessage( { target: 'bridge-swarm-native', action: 'unregisterDashboard' }, () => {} ); }); } // Initialize async function init() { log('Dashboard initializing...'); setupNavigation(); setupEvents(); loadSettings(); await refresh(); // Auto-refresh every 2 seconds setInterval(refresh, 2000); } init();