/** * BridgeSwarm Control Center — premium dashboard UI */ const SETTINGS_KEY = 'bridgeSwarmSettings'; const DASHBOARD_PORT = 'bridgeswarm-dashboard'; const DEFAULT_SETTINGS = { defaultAppName: 'bridge-swarm', defaultMaxPeers: 0, defaultRequestTimeoutMs: 0, readyTimeoutMs: 0, defaultCapJobTimeoutMs: 120000, disableOnFileUrls: false, notifyOnDisconnect: false, debug: false, examplesServerEnabled: false, qvacEnabled: false, examplesServerPort: 4173, defaultFirewallMode: 'off', defaultFirewallKeys: [], dashboardRefreshMs: 2000, capabilityOrigins: [], agentAlwaysApproveOrigins: [], agentWorkspaceRoots: [], }; let currentState = null; let settings = { ...DEFAULT_SETTINGS }; let settingsLoading = false; let settingsSaveTimer = null; let refreshTimer = null; let lastRefreshAt = 0; let dashPort = null; /** @type {any[]} */ let allLogs = []; let paused = false; let follow = true; let unseenErrors = 0; const levelFilter = new Set(['info', 'warn', 'error']); const categoryFilter = new Set(['host', 'swarm', 'conn', 'cap', 'examples', 'settings']); let searchQuery = ''; let metaFilter = null; const COPY_SVG = ''; function $(id) { return document.getElementById(id); } function showToast(msg, kind) { const el = $('toast'); if (!el) return; el.textContent = msg; el.className = 'toast show' + (kind === 'err' ? ' err' : kind === 'ok' ? ' ok' : ''); clearTimeout(showToast._t); showToast._t = setTimeout(() => el.classList.remove('show'), 2200); } function formatQvacStatus(q) { if (!q) return '—'; if (q.enabled === false) return 'disabled (Settings)'; if (q.skipped) return 'skipped'; if (!q.available) return q.error ? 'unavailable' : 'not loaded'; const parts = [q.device || 'cpu']; if (q.modelId) parts.push(q.friendlyId || q.modelId); if (q.backend) parts.unshift(q.backend); return parts.join(' · '); } function timeAgo(ts) { const seconds = Math.floor((Date.now() - ts) / 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'; } 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'; const hours = Math.floor(minutes / 60); return hours + 'h ' + (minutes % 60) + 'm'; } function truncate(str, len = 14) { if (!str) return ''; return str.length > len ? str.slice(0, len) + '…' : str; } function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str == null ? '' : String(str); return d.innerHTML; } function idCell(full, len = 14) { const safe = escapeHtml(full || ''); return `${escapeHtml(truncate(full, len))}`; } function sendBg(action, payload) { return new Promise((resolve) => { chrome.runtime.sendMessage( { target: 'bridge-swarm-native', action, payload }, (response) => { if (chrome.runtime.lastError) { resolve({ ok: false, error: chrome.runtime.lastError.message }); return; } resolve(response || { ok: false }); } ); }); } function connectDashboardPort() { try { dashPort = chrome.runtime.connect({ name: DASHBOARD_PORT }); } catch (e) { console.warn('Dashboard port failed', e); return; } dashPort.onMessage.addListener((msg) => { if (!msg) return; if (msg.type === 'bridge-swarm-logs') { if (msg.logs) { if (!paused) { const prevLen = allLogs.length; allLogs = msg.logs; if (allLogs.length > prevLen) { const added = allLogs.slice(prevLen); for (const e of added) { if (e.level === 'error') unseenErrors++; } } updateErrorBadge(); renderLogs(); renderRecentActivity(); } } else if (msg.entry && !paused) { allLogs.push(msg.entry); if (allLogs.length > 500) allLogs.shift(); if (msg.entry.level === 'error') { unseenErrors++; updateErrorBadge(); } renderLogs(); renderRecentActivity(); } } if (msg.type === 'bridge-swarm-state' && msg.state) { applyState(msg.state); } }); dashPort.onDisconnect.addListener(() => { dashPort = null; setTimeout(connectDashboardPort, 1000); }); } async function fetchState() { await refreshHostSnapshotSoft(); return sendBg('getState'); } async function refreshHostSnapshotSoft() { /* getState already refreshes snapshot in BG */ } function applyState(state) { if (!state) return; currentState = state; lastRefreshAt = Date.now(); updateOverview(state); updateSwarmsTable(state); updateConnectionsTable(state); updateTabsTable(state); $('lastUpdated').textContent = 'Updated ' + new Date().toLocaleTimeString(); $('sidebarHostHint').textContent = state.hostConnected ? 'Host: connected' : state.health?.reconnectAttempt ? `Host: reconnecting (#${state.health.reconnectAttempt})` : 'Host: disconnected'; } function updateOverview(state) { const connected = !!state.hostConnected; const reconnecting = !connected && (state.stats?.reconnectAttempt > 0 || state.health?.reconnectAttempt > 0); const pill = $('overviewPill'); const pillText = $('overviewPillText'); if (connected) { pill.className = 'status-pill ok'; pillText.textContent = 'Host connected'; } else if (reconnecting) { pill.className = 'status-pill warn'; pillText.textContent = 'Reconnecting…'; } else { pill.className = 'status-pill err'; pillText.textContent = 'Host disconnected'; } const h = state.health || {}; const health = $('healthCard'); if (!connected) { health.innerHTML = `

Native host not connected

${escapeHtml(h.lastDisconnectReason || 'Install or repair the BridgeSwarm native host, then reload this panel.')}

${h.reconnectAttempt ? `

Reconnect attempt #${h.reconnectAttempt} · next in ~${Math.round((state.stats?.reconnectDelayMs || 0) / 1000)}s

` : ''}
Extensionv${escapeHtml(h.extensionVersion || '—')}
Host${escapeHtml(h.hostVersion || '—')}
`; $('btnRetryHint')?.addEventListener('click', () => refresh()); } else { health.innerHTML = `
StatusConnected
Extensionv${escapeHtml(h.extensionVersion || '—')}
Hostv${escapeHtml(h.hostVersion || '—')}
Storage${escapeHtml(h.storagePath || '—')}
Capability packs${escapeHtml((h.packs || []).join(', ') || 'none')}
QVAC${escapeHtml(formatQvacStatus(h.qvac))}
Pending RPCs${state.stats?.pendingRequests ?? 0}
`; } $('dashSwarms').textContent = state.stats?.totalSwarms ?? 0; $('dashConnections').textContent = state.stats?.totalConnections ?? 0; $('dashTabs').textContent = state.tabs?.length ?? 0; $('dashUptime').textContent = formatUptime(Date.now() - (state.stats?.uptime || Date.now())); $('swarmCount').textContent = state.stats?.totalSwarms ?? 0; $('connCount').textContent = state.stats?.totalConnections ?? 0; updateOverviewExamples(state); updateRecentConnections(state); renderCapabilitiesPage(state); // Empty swarm guidance if (connected && (state.stats?.totalSwarms || 0) === 0) { /* shown via tables */ } } function renderCapabilitiesPage(state) { const h = state.health || {}; const packs = h.packs || []; const packsEl = $('capPacksBody'); if (packsEl) { if (!packs.length) { packsEl.innerHTML = '
No packs reported
'; } else { packsEl.innerHTML = packs .map((id) => `
${escapeHtml(id)}installed
`) .join(''); } } const q = h.qvac || {}; const qEl = $('capQvacBody'); const qToggle = $('capQvacToggle'); const qOn = settings.qvacEnabled === true; if (qToggle) { qToggle.classList.toggle('active', qOn); qToggle.setAttribute('aria-checked', qOn ? 'true' : 'false'); } if (qEl) { qEl.innerHTML = `
Enabled${qOn ? 'yes' : 'no (Settings)'}
Available${q.available ? 'yes' : 'no'}
Device${escapeHtml(q.device || '—')}
Backend${escapeHtml(q.backend || q.backendId || '—')}
GPU${escapeHtml(q.deviceName || (q.vram ? String(q.vram) : '—'))}
Model${escapeHtml(q.friendlyId || q.modelId || 'none')}
Plugins${escapeHtml((q.plugins || []).join(', ') || '—')}
OpenAI${escapeHtml(q.openaiUrl || 'off')}
${q.error ? `
Error${escapeHtml(q.error)}
` : ''}
`; } const a = h.agent || {}; const aEl = $('capAgentBody'); if (aEl) { aEl.innerHTML = `
Sessions${a.sessionCount ?? 0}
Granted roots${escapeHtml((a.grantedRoots || []).join(', ') || 'storage default')}
MCP servers${a.mcpCount ?? 0}
`; } } function updateOverviewExamples(state) { const ex = state.examples || {}; const toggle = $('overviewExamplesToggle'); const body = $('overviewExamplesBody'); const enabled = settings.examplesServerEnabled === true; toggle.classList.toggle('active', enabled); toggle.setAttribute('aria-checked', enabled ? 'true' : 'false'); if (!enabled) { body.innerHTML = `

Serve bundled demos at http://127.0.0.1:${settings.examplesServerPort || 4173}/ via the native host.

`; $('btnEnableExamplesOverview')?.addEventListener('click', () => { const st = $('toggleExamplesServer'); if (st) st.classList.add('active'); toggle.classList.add('active'); settings.examplesServerEnabled = true; chrome.storage.local.set({ [SETTINGS_KEY]: { ...settings, examplesServerEnabled: true } }, () => { updateOverviewExamples(currentState || { examples: {}, hostConnected: !!currentState?.hostConnected }); updateExamplesSettingsPanel(); showToast('Examples server enabled', 'ok'); }); }); return; } const url = ex.url || `http://127.0.0.1:${settings.examplesServerPort || 4173}/`; let status = 'Starting…'; let statusClass = 'muted'; if (ex.error) { statusClass = ''; const busy = /already in use|EADDRINUSE/i.test(ex.error); status = `${escapeHtml(ex.error)}`; } else if (ex.running) { status = ex.reused ? `Running (already on this port)` : `Running`; } else if (!state.hostConnected) { status = 'Waiting for native host…'; } body.innerHTML = `
Status${status}
URL${escapeHtml(url)}
`; void statusClass; $('btnOpenExOverview')?.addEventListener('click', () => chrome.tabs.create({ url })); $('btnCopyExOverview')?.addEventListener('click', async () => { try { await navigator.clipboard.writeText(url); showToast('URL copied', 'ok'); } catch (_) { showToast('Copy failed', 'err'); } }); } function updateRecentConnections(state) { const connections = []; for (const swarm of state.swarms || []) { for (const conn of swarm.connections || []) connections.push(conn); } connections.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); const tbody = $('recentConnections'); if (!connections.length) { const connected = state.hostConnected; tbody.innerHTML = connected ? `

No peers yet

Open the examples gallery and join a topic in two tabs.

` : `Waiting for host…`; return; } tbody.innerHTML = connections .slice(0, 8) .map( (c) => ` ${idCell(c.connId)} ${idCell(c.swarmId, 12)} ${idCell(c.peerKey, 12)} ${timeAgo(c.createdAt)} ` ) .join(''); } function updateSwarmsTable(state) { const tbody = $('swarmsTable'); const swarms = state.swarms || []; if (!swarms.length) { tbody.innerHTML = `

No active swarms

Swarms appear when a page creates new BridgeSwarm() and joins a topic.

`; return; } tbody.innerHTML = swarms .map((s) => { const fw = s.firewall ? `${s.firewall.mode}${s.firewall.keyCount ? ` (${s.firewall.keyCount})` : ''}` : '—'; return ` ${idCell(s.swarmId, 18)} ${escapeHtml(s.appName || '—')} ${s.peerCount ?? s.connections?.length ?? 0} ${s.tabCount ?? 0} ${escapeHtml(fw)} ${s.createdAt ? timeAgo(s.createdAt) : '—'} `; }) .join(''); } function updateConnectionsTable(state) { const connections = []; for (const swarm of state.swarms || []) { for (const conn of swarm.connections || []) connections.push(conn); } connections.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); const tbody = $('connectionsTable'); if (!connections.length) { tbody.innerHTML = `

No active connections

Peer sockets show up after Hyperswarm discovers another peer on the same topic.

`; return; } tbody.innerHTML = connections .map((c) => { const topics = (c.topics || []).map((t) => truncate(t, 8)).join(', ') || '—'; return ` ${idCell(c.connId, 18)} ${idCell(c.swarmId, 12)} ${idCell(c.peerKey, 16)} ${escapeHtml(topics)} ${timeAgo(c.createdAt)} `; }) .join(''); } function updateTabsTable(state) { const tbody = $('tabsTable'); const tabs = state.tabs || []; if (!tabs.length) { tbody.innerHTML = `No BridgeSwarm tabs`; return; } tbody.innerHTML = tabs .map((t) => { const title = t.title || `Tab ${t.tabId}`; const url = t.url ? truncate(t.url, 40) : ''; return ` ${t.tabId}
${escapeHtml(title)}
${escapeHtml(url)}
${(t.swarmIds || []).map((id) => idCell(id, 10)).join(' ') || '—'} `; }) .join(''); } /* ——— Logs ——— */ function updateErrorBadge() { const badge = $('logErrorBadge'); if (!badge) return; if (unseenErrors > 0) { badge.hidden = false; badge.textContent = String(unseenErrors); badge.classList.add('warn'); } else { badge.hidden = true; } } function filteredLogs() { return allLogs.filter((e) => { const level = e.level || 'info'; const cat = e.category || 'host'; if (!levelFilter.has(level)) return false; if (!categoryFilter.has(cat)) return false; if (metaFilter) { const m = e.meta || {}; const hay = [e.message, m.swarmId, m.connId, m.requestId, m.jobId].join(' '); if (!hay.includes(metaFilter)) return false; } if (searchQuery) { const q = searchQuery.toLowerCase(); const blob = JSON.stringify(e).toLowerCase(); if (!blob.includes(q)) return false; } return true; }); } function linkifyMessage(msg) { return escapeHtml(msg).replace(/(swarm_[a-zA-Z0-9]+|conn_[a-zA-Z0-9]+|req_[a-zA-Z0-9]+|(?:job|session)_[a-zA-Z0-9]+)/g, (id) => { return `${id}`; }); } function renderLogs() { const container = $('logsContainer'); if (!container) return; const rows = filteredLogs(); $('logsCount').textContent = `${rows.length} / ${allLogs.length} events`; $('logsFilterHint').textContent = metaFilter ? `Filter: ${metaFilter}` : searchQuery ? `Search: ${searchQuery}` : 'Showing filtered'; if (!rows.length) { container.innerHTML = '
No log events match filters
'; return; } const slice = rows.slice(-400); container.innerHTML = slice .map((e) => { const ts = e.ts || e.timestamp || Date.now(); const time = new Date(ts).toLocaleTimeString(); const level = e.level || 'info'; const cat = e.category || 'host'; const meta = e.meta && Object.keys(e.meta).length ? escapeHtml(JSON.stringify(e.meta, null, 2)) : ''; return `
${time} ${level} ${cat} ${linkifyMessage(e.message || '')}${meta ? `
${meta}
` : ''}
`; }) .join(''); if (follow) container.scrollTop = container.scrollHeight; } function renderRecentActivity() { const el = $('recentActivity'); if (!el) return; const recent = allLogs.slice(-12).reverse(); if (!recent.length) { el.innerHTML = '
No events yet
'; return; } el.innerHTML = recent .map((e) => { const ts = e.ts || e.timestamp; return `
${new Date(ts).toLocaleTimeString()} ${escapeHtml(truncate(e.message, 80))}
`; }) .join(''); } /* ——— Settings ——— */ function renderSettingsForm() { const root = $('settingsRoot'); if (!root) return; const s = settings; root.innerHTML = `

Notifications & debug

Host disconnect notification
Browser notification when the native host drops
Debug mode
Verbose bridge logging in the Activity stream and page console
Disable on file:// URLs
Skip injecting BridgeSwarm on local files

Swarm defaults

Default app name
Used when a page omits appName
Default max peers
0 = unlimited
Request timeout (ms)
Default BridgeSwarm.request timeout; 0 = none
Ready timeout (ms)
BridgeSwarm.ready() timeout; 0 = wait forever
Capability job timeout (ms)
Default wait for media/transcode jobs

Default peer firewall

Mode
Applied automatically when a swarm initializes
Peer keys
One public-key hex per line

Capability origins

Packs (fs, media, qvac, agent, …) only run for listed origins. The examples server origin is always included.

Allowed origins
One origin per line, e.g. https://app.example.com
Agent always-approve
Origins that may skip agent permission prompts (trusted apps only)
Agent workspace roots
Absolute host paths the agent may read/write in addition to storage

QVAC

Enable QVAC
Local on-device inference and the grok-class agent. Off until you turn this on.

Examples server

Enable examples server
Native host serves demos on localhost only
Port
Bound to 127.0.0.1 only

Panel

Refresh interval (ms)
How often Overview polls getState (1000–10000)

About

BridgeSwarm — Hyperswarm in the browser via a Bare native messaging host.

Version: · License: AGPL-3.0

Owned and engineered by HoneyPeer, LLC (DeKalb County, Georgia, USA).

Legal: legal@honeypeer.com

Settings autosave. Reset restores defaults including examples server and QVAC off.

`; $('toggleNotify')?.classList.toggle('active', s.notifyOnDisconnect === true); $('toggleDebug')?.classList.toggle('active', s.debug === true); $('toggleDisableFileUrls')?.classList.toggle('active', s.disableOnFileUrls === true); $('toggleExamplesServer')?.classList.toggle('active', s.examplesServerEnabled === true); $('toggleQvacEnabled')?.classList.toggle('active', s.qvacEnabled === true); const fw = $('defaultFirewallMode'); if (fw) fw.value = s.defaultFirewallMode || 'off'; const ver = chrome.runtime.getManifest?.().version; if (ver && $('aboutVersion')) $('aboutVersion').textContent = ver; root.querySelectorAll('.toggle').forEach((toggle) => { toggle.addEventListener('click', () => { toggle.classList.toggle('active'); scheduleSaveSettings(); if (toggle.id === 'toggleExamplesServer' || toggle.id === 'overviewExamplesToggle') { updateExamplesSettingsPanel(); } }); }); [ 'defaultAppName', 'defaultMaxPeers', 'defaultRequestTimeoutMs', 'readyTimeoutMs', 'defaultCapJobTimeoutMs', 'examplesServerPort', 'dashboardRefreshMs', 'defaultFirewallMode', 'defaultFirewallKeys', 'capabilityOrigins', 'agentAlwaysApproveOrigins', 'agentWorkspaceRoots', ].forEach((id) => { const el = $(id); if (!el) return; el.addEventListener('change', scheduleSaveSettings); el.addEventListener('input', scheduleSaveSettings); }); updateExamplesSettingsPanel(); } function updateExamplesSettingsPanel() { const panel = $('examplesServerPanel'); if (!panel) return; const on = $('toggleExamplesServer')?.classList.contains('active'); panel.style.display = on ? 'block' : 'none'; if (!on) return; const port = parseInt($('examplesServerPort')?.value, 10) || 4173; const url = `http://127.0.0.1:${port}/`; panel.innerHTML = `
${escapeHtml(url)}
`; $('btnOpenExamples')?.addEventListener('click', () => chrome.tabs.create({ url })); $('btnCopyExamplesUrl')?.addEventListener('click', async () => { try { await navigator.clipboard.writeText(url); showToast('URL copied', 'ok'); } catch (_) { showToast('Copy failed', 'err'); } }); } function collectSettings() { const keysRaw = ($('defaultFirewallKeys')?.value || '') .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean); const originLines = (id) => ($(id)?.value || '') .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean); const port = parseInt($('examplesServerPort')?.value, 10); const refresh = parseInt($('dashboardRefreshMs')?.value, 10); const reqTimeout = parseInt($('defaultRequestTimeoutMs')?.value, 10); const readyTimeout = parseInt($('readyTimeoutMs')?.value, 10); const capTimeout = parseInt($('defaultCapJobTimeoutMs')?.value, 10); const maxPeers = parseInt($('defaultMaxPeers')?.value, 10); if (isNaN(reqTimeout) || reqTimeout < 0 || reqTimeout > 300000) { showToast('Request timeout must be 0–300000', 'err'); return null; } if (isNaN(readyTimeout) || readyTimeout < 0 || readyTimeout > 60000) { showToast('Ready timeout must be 0–60000', 'err'); return null; } if (isNaN(capTimeout) || capTimeout < 1000 || capTimeout > 600000) { showToast('Cap job timeout must be 1000–600000', 'err'); return null; } if (isNaN(port) || port < 1024 || port > 65535) { showToast('Examples port must be 1024–65535', 'err'); return null; } if (isNaN(refresh) || refresh < 1000 || refresh > 10000) { showToast('Refresh interval must be 1000–10000', 'err'); return null; } return { defaultAppName: ($('defaultAppName')?.value || '').trim() || DEFAULT_SETTINGS.defaultAppName, defaultMaxPeers: isNaN(maxPeers) || maxPeers < 0 ? 0 : maxPeers, defaultRequestTimeoutMs: reqTimeout, readyTimeoutMs: readyTimeout, defaultCapJobTimeoutMs: capTimeout, disableOnFileUrls: $('toggleDisableFileUrls')?.classList.contains('active') || false, notifyOnDisconnect: $('toggleNotify')?.classList.contains('active') || false, debug: $('toggleDebug')?.classList.contains('active') || false, examplesServerEnabled: $('toggleExamplesServer')?.classList.contains('active') || false, qvacEnabled: $('toggleQvacEnabled')?.classList.contains('active') || false, examplesServerPort: port, defaultFirewallMode: $('defaultFirewallMode')?.value || 'off', defaultFirewallKeys: keysRaw, dashboardRefreshMs: refresh, capabilityOrigins: originLines('capabilityOrigins'), agentAlwaysApproveOrigins: originLines('agentAlwaysApproveOrigins'), agentWorkspaceRoots: originLines('agentWorkspaceRoots'), }; } function saveSettings(quiet) { const next = collectSettings(); if (!next) return; const refreshChanged = next.dashboardRefreshMs !== settings.dashboardRefreshMs; settings = next; chrome.storage.local.set({ [SETTINGS_KEY]: settings }, () => { if (chrome.runtime.lastError) { showToast(chrome.runtime.lastError.message, 'err'); return; } if (!quiet) showToast('Saved', 'ok'); updateExamplesSettingsPanel(); if (refreshChanged) scheduleRefreshLoop(); // sync overview toggle $('overviewExamplesToggle')?.classList.toggle('active', settings.examplesServerEnabled); $('capQvacToggle')?.classList.toggle('active', settings.qvacEnabled === true); $('capQvacToggle')?.setAttribute('aria-checked', settings.qvacEnabled === true ? 'true' : 'false'); }); } function scheduleSaveSettings() { if (settingsLoading) return; clearTimeout(settingsSaveTimer); settingsSaveTimer = setTimeout(() => saveSettings(true), 280); } function loadSettings() { settingsLoading = true; chrome.storage.local.get(SETTINGS_KEY, (result) => { settings = { ...DEFAULT_SETTINGS, ...(result[SETTINGS_KEY] || {}) }; settingsLoading = false; renderSettingsForm(); scheduleRefreshLoop(); const ver = chrome.runtime.getManifest?.().version; if (ver) { if ($('extVersion')) $('extVersion').textContent = 'v' + ver; if ($('aboutVersion')) $('aboutVersion').textContent = ver; } }); } async function destroySwarm(swarmId) { if (!confirm(`Destroy swarm "${swarmId}"? This disconnects its peers.`)) return; const res = await sendBg('destroySwarm', { swarmId }); if (res && res.ok !== false && !res.error) { showToast('Swarm destroyed', 'ok'); refresh(); } else { showToast(res?.error || 'Destroy failed', 'err'); } } async function refresh() { const res = await fetchState(); if (res && res.ok && res.state) applyState(res.state); } function scheduleRefreshLoop() { clearInterval(refreshTimer); const ms = Math.min(10000, Math.max(1000, settings.dashboardRefreshMs || 2000)); refreshTimer = setInterval(() => refresh(), ms); } function goToPage(page) { document.querySelectorAll('.nav-item').forEach((i) => i.classList.toggle('active', i.dataset.page === page)); document.querySelectorAll('.page').forEach((p) => p.classList.toggle('active', p.id === `page-${page}`)); if (page === 'activity') { unseenErrors = 0; updateErrorBadge(); } if (location.hash !== '#' + page) { history.replaceState(null, '', '#' + page); } } function setupNavigation() { document.querySelectorAll('.nav-item').forEach((item) => { item.addEventListener('click', () => goToPage(item.dataset.page)); }); document.querySelectorAll('[data-goto]').forEach((btn) => { btn.addEventListener('click', () => goToPage(btn.getAttribute('data-goto'))); }); // delegated goto / destroy / focus / copy document.body.addEventListener('click', async (e) => { const t = e.target.closest('[data-goto], [data-destroy], [data-focus-tab], [data-copy], [data-filter-id], .log-row'); if (!t) return; if (t.hasAttribute('data-goto')) { goToPage(t.getAttribute('data-goto')); return; } if (t.hasAttribute('data-destroy')) { destroySwarm(t.getAttribute('data-destroy')); return; } if (t.hasAttribute('data-focus-tab')) { const tabId = parseInt(t.getAttribute('data-focus-tab'), 10); await sendBg('focusTab', { tabId }); return; } if (t.hasAttribute('data-copy')) { try { await navigator.clipboard.writeText(t.getAttribute('data-copy')); showToast('Copied', 'ok'); } catch (_) { showToast('Copy failed', 'err'); } return; } if (t.hasAttribute('data-filter-id')) { metaFilter = t.getAttribute('data-filter-id'); $('logSearch').value = metaFilter; searchQuery = metaFilter; goToPage('activity'); renderLogs(); return; } if (t.classList.contains('log-row')) { t.classList.toggle('expanded'); } }); } function setupLogsUi() { $('levelChips')?.addEventListener('click', (e) => { const chip = e.target.closest('[data-level]'); if (!chip) return; const level = chip.dataset.level; if (levelFilter.has(level)) levelFilter.delete(level); else levelFilter.add(level); chip.classList.toggle('active'); renderLogs(); }); $('categoryChips')?.addEventListener('click', (e) => { const chip = e.target.closest('[data-cat]'); if (!chip) return; const cat = chip.dataset.cat; if (categoryFilter.has(cat)) categoryFilter.delete(cat); else categoryFilter.add(cat); chip.classList.toggle('active'); renderLogs(); }); $('logSearch')?.addEventListener('input', (e) => { searchQuery = e.target.value.trim(); metaFilter = null; renderLogs(); }); $('btnPauseLogs')?.addEventListener('click', () => { paused = !paused; $('btnPauseLogs').textContent = paused ? 'Resume' : 'Pause'; if (!paused) sendBg('getLogs').then((r) => { if (r?.logs) { allLogs = r.logs; renderLogs(); } }); }); $('btnFollowLogs')?.addEventListener('click', () => { follow = !follow; $('btnFollowLogs').textContent = 'Follow: ' + (follow ? 'ON' : 'OFF'); if (follow) { const c = $('logsContainer'); if (c) c.scrollTop = c.scrollHeight; } }); $('logsContainer')?.addEventListener('scroll', () => { const c = $('logsContainer'); if (!c) return; const atBottom = c.scrollHeight - c.scrollTop - c.clientHeight < 40; if (!atBottom && follow) { follow = false; $('btnFollowLogs').textContent = 'Follow: OFF'; } }); $('btnClearLogs')?.addEventListener('click', async () => { await sendBg('clearLogs'); allLogs = []; renderLogs(); renderRecentActivity(); showToast('Logs cleared', 'ok'); }); $('btnExportJson')?.addEventListener('click', () => exportLogs('json')); $('btnExportTxt')?.addEventListener('click', () => exportLogs('txt')); } function exportLogs(kind) { const rows = filteredLogs(); let blob; let name; if (kind === 'json') { blob = new Blob([JSON.stringify(rows, null, 2)], { type: 'application/json' }); name = 'bridgeswarm-logs.json'; } else { const text = rows .map((e) => { const ts = new Date(e.ts || e.timestamp).toISOString(); return `${ts}\t${e.level}\t${e.category}\t${e.message}`; }) .join('\n'); blob = new Blob([text], { type: 'text/plain' }); name = 'bridgeswarm-logs.txt'; } const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); URL.revokeObjectURL(a.href); showToast('Exported ' + rows.length + ' events', 'ok'); } function setupEvents() { $('btnResetSettings')?.addEventListener('click', () => { settings = { ...DEFAULT_SETTINGS }; chrome.storage.local.set({ [SETTINGS_KEY]: settings }, () => { renderSettingsForm(); showToast('Reset to defaults', 'ok'); scheduleRefreshLoop(); }); }); $('overviewExamplesToggle')?.addEventListener('click', () => { const t = $('overviewExamplesToggle'); t.classList.toggle('active'); const enabled = t.classList.contains('active'); const st = $('toggleExamplesServer'); if (st) st.classList.toggle('active', enabled); settings.examplesServerEnabled = enabled; chrome.storage.local.set({ [SETTINGS_KEY]: { ...settings, examplesServerEnabled: enabled } }, () => { updateOverviewExamples(currentState || { examples: {}, hostConnected: !!currentState?.hostConnected }); updateExamplesSettingsPanel(); showToast(enabled ? 'Examples server enabled' : 'Examples server disabled', 'ok'); }); }); $('overviewExamplesToggle')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.currentTarget.click(); } }); function persistQvacEnabled(enabled) { const st = $('toggleQvacEnabled'); if (st) st.classList.toggle('active', enabled); const cap = $('capQvacToggle'); if (cap) { cap.classList.toggle('active', enabled); cap.setAttribute('aria-checked', enabled ? 'true' : 'false'); } settings.qvacEnabled = enabled; chrome.storage.local.set({ [SETTINGS_KEY]: { ...settings, qvacEnabled: enabled } }, () => { if (currentState) renderCapabilitiesPage(currentState); showToast(enabled ? 'QVAC enabled' : 'QVAC disabled', 'ok'); }); } $('capQvacToggle')?.addEventListener('click', () => { persistQvacEnabled(settings.qvacEnabled !== true); }); $('capQvacToggle')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.currentTarget.click(); } }); chrome.storage.onChanged.addListener((changes, area) => { if (area !== 'local' || !changes[SETTINGS_KEY]) return; settingsLoading = true; settings = { ...DEFAULT_SETTINGS, ...(changes[SETTINGS_KEY].newValue || {}) }; renderSettingsForm(); settingsLoading = false; if (currentState) { updateOverviewExamples(currentState); renderCapabilitiesPage(currentState); } }); setupLogsUi(); } async function init() { const hash = (location.hash || '').replace('#', ''); setupNavigation(); setupEvents(); loadSettings(); connectDashboardPort(); const logsRes = await sendBg('getLogs'); if (logsRes?.logs) { allLogs = logsRes.logs; renderLogs(); renderRecentActivity(); } await refresh(); if (hash && $(`page-${hash}`)) goToPage(hash); } init();