Files
BridgeSwarm/extension/dashboard.js
T
snxraven 693814f46b
CI / Build & Test (push) Failing after 52s
Add Discordjs
2026-09-04 22:10:35 -04:00

1162 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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,
discordEnabled: 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 =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></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 desktopVisionLabel(dv) {
if (!dv || !dv.live) return 'off';
const fps = dv.fps ? String(dv.fps) + ' fps' : 'live';
return dv.stale ? fps + ' (stale)' : fps;
}
function idCell(full, len = 14) {
const safe = escapeHtml(full || '');
return `<span class="id-cell" title="${safe}"><code>${escapeHtml(truncate(full, len))}</code><button type="button" class="copy-btn" data-copy="${safe}" title="Copy">${COPY_SVG}</button></span>`;
}
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 = `
<div class="empty-state">
<h4>Native host not connected</h4>
<p>${escapeHtml(h.lastDisconnectReason || 'Install or repair the BridgeSwarm native host, then reload this panel.')}</p>
<div class="btn-row" style="justify-content:center">
<button type="button" class="btn btn-primary" id="btnRetryHint">Retry connection</button>
</div>
${h.reconnectAttempt ? `<p class="muted" style="margin-top:0.75rem">Reconnect attempt #${h.reconnectAttempt} · next in ~${Math.round((state.stats?.reconnectDelayMs || 0) / 1000)}s</p>` : ''}
</div>
<div class="health" style="margin-top:1rem;border-top:1px solid var(--border);padding-top:0.85rem">
<div class="health-row"><span class="k">Extension</span><span class="v">v${escapeHtml(h.extensionVersion || '—')}</span></div>
<div class="health-row"><span class="k">Host</span><span class="v">${escapeHtml(h.hostVersion || '—')}</span></div>
</div>`;
$('btnRetryHint')?.addEventListener('click', () => refresh());
} else {
health.innerHTML = `
<div class="health">
<div class="health-row"><span class="k">Status</span><span class="v" style="color:var(--accent)">Connected</span></div>
<div class="health-row"><span class="k">Extension</span><span class="v">v${escapeHtml(h.extensionVersion || '—')}</span></div>
<div class="health-row"><span class="k">Host</span><span class="v">v${escapeHtml(h.hostVersion || '—')}</span></div>
<div class="health-row"><span class="k">Storage</span><span class="v">${escapeHtml(h.storagePath || '—')}</span></div>
<div class="health-row"><span class="k">Capability packs</span><span class="v">${escapeHtml((h.packs || []).join(', ') || 'none')}</span></div>
<div class="health-row"><span class="k">QVAC</span><span class="v">${escapeHtml(formatQvacStatus(h.qvac))}</span></div>
<div class="health-row"><span class="k">Pending RPCs</span><span class="v">${state.stats?.pendingRequests ?? 0}</span></div>
</div>`;
}
$('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 = '<div class="empty-state muted">No packs reported</div>';
} else {
packsEl.innerHTML = packs
.map((id) => `<div class="health-row"><span class="k">${escapeHtml(id)}</span><span class="v" style="color:var(--accent)">installed</span></div>`)
.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 = `
<div class="health">
<div class="health-row"><span class="k">Enabled</span><span class="v">${qOn ? 'yes' : 'no (Settings)'}</span></div>
<div class="health-row"><span class="k">Available</span><span class="v">${q.available ? 'yes' : 'no'}</span></div>
<div class="health-row"><span class="k">Device</span><span class="v">${escapeHtml(q.device || '—')}</span></div>
<div class="health-row"><span class="k">Backend</span><span class="v">${escapeHtml(q.backend || q.backendId || '—')}</span></div>
<div class="health-row"><span class="k">GPU</span><span class="v">${escapeHtml(q.deviceName || (q.vram ? String(q.vram) : '—'))}</span></div>
<div class="health-row"><span class="k">Model</span><span class="v">${escapeHtml(q.friendlyId || q.modelId || 'none')}</span></div>
<div class="health-row"><span class="k">Vision</span><span class="v">${q.modelId || q.friendlyId ? (q.vision ? 'yes' : 'no') : '—'}</span></div>
<div class="health-row"><span class="k">Desktop</span><span class="v">${escapeHtml(desktopVisionLabel(q.desktopVision))}</span></div>
<div class="health-row"><span class="k">Plugins</span><span class="v">${escapeHtml((q.plugins || []).join(', ') || '—')}</span></div>
<div class="health-row"><span class="k">OpenAI</span><span class="v">${escapeHtml(q.openaiUrl || 'off')}</span></div>
${q.error ? `<div class="health-row"><span class="k">Error</span><span class="v">${escapeHtml(q.error)}</span></div>` : ''}
</div>`;
}
const d = h.discord || {};
const dEl = $('capDiscordBody');
const dToggle = $('capDiscordToggle');
const dOn = settings.discordEnabled === true;
if (dToggle) {
dToggle.classList.toggle('active', dOn);
dToggle.setAttribute('aria-checked', dOn ? 'true' : 'false');
}
if (dEl) {
dEl.innerHTML = `
<div class="health">
<div class="health-row"><span class="k">Enabled</span><span class="v">${dOn ? 'yes' : 'no (Settings)'}</span></div>
<div class="health-row"><span class="k">Loaded</span><span class="v">${d.loaded ? 'yes' : 'no'}</span></div>
<div class="health-row"><span class="k">Clients</span><span class="v">${d.clients ?? 0} / ${d.maxClients ?? 4}</span></div>
${d.loadError ? `<div class="health-row"><span class="k">Error</span><span class="v">${escapeHtml(d.loadError)}</span></div>` : ''}
</div>`;
}
const a = h.agent || {};
const aEl = $('capAgentBody');
if (aEl) {
aEl.innerHTML = `
<div class="health">
<div class="health-row"><span class="k">Sessions</span><span class="v">${a.sessionCount ?? 0}</span></div>
<div class="health-row"><span class="k">Granted roots</span><span class="v">${escapeHtml((a.grantedRoots || []).join(', ') || 'storage default')}</span></div>
<div class="health-row"><span class="k">MCP servers</span><span class="v">${a.mcpCount ?? 0}</span></div>
</div>`;
}
}
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 = `
<p class="muted" style="font-size:0.85rem;line-height:1.45;margin-bottom:0.85rem">
Serve bundled demos at <span class="mono">http://127.0.0.1:${settings.examplesServerPort || 4173}/</span> via the native host.
</p>
<button type="button" class="btn btn-primary btn-sm" id="btnEnableExamplesOverview">Enable examples server</button>`;
$('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 = `<span style="color:var(--${busy ? 'warn' : 'danger'})">${escapeHtml(ex.error)}</span>`;
} else if (ex.running) {
status = ex.reused
? `<span style="color:var(--accent)">Running</span> <span class="muted">(already on this port)</span>`
: `<span style="color:var(--accent)">Running</span>`;
} else if (!state.hostConnected) {
status = 'Waiting for native host…';
}
body.innerHTML = `
<div class="health">
<div class="health-row"><span class="k">Status</span><span class="v">${status}</span></div>
<div class="health-row"><span class="k">URL</span><span class="v">${escapeHtml(url)}</span></div>
</div>
<div class="btn-row" style="margin-top:0.85rem">
<button type="button" class="btn btn-primary btn-sm" id="btnOpenExOverview">Open examples</button>
<button type="button" class="btn btn-secondary btn-sm" id="btnCopyExOverview">Copy URL</button>
</div>`;
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
? `<tr><td colspan="4"><div class="empty-state"><h4>No peers yet</h4><p>Open the examples gallery and join a topic in two tabs.</p><button type="button" class="btn btn-primary btn-sm" data-goto="settings">Enable examples in Settings</button></div></td></tr>`
: `<tr><td colspan="4" class="muted" style="padding:1rem">Waiting for host…</td></tr>`;
return;
}
tbody.innerHTML = connections
.slice(0, 8)
.map(
(c) => `<tr>
<td>${idCell(c.connId)}</td>
<td>${idCell(c.swarmId, 12)}</td>
<td>${idCell(c.peerKey, 12)}</td>
<td class="time-ago">${timeAgo(c.createdAt)}</td>
</tr>`
)
.join('');
}
function updateSwarmsTable(state) {
const tbody = $('swarmsTable');
const swarms = state.swarms || [];
if (!swarms.length) {
tbody.innerHTML = `<tr><td colspan="7"><div class="empty-state"><h4>No active swarms</h4><p>Swarms appear when a page creates <code>new BridgeSwarm()</code> and joins a topic.</p></div></td></tr>`;
return;
}
tbody.innerHTML = swarms
.map((s) => {
const fw = s.firewall ? `${s.firewall.mode}${s.firewall.keyCount ? ` (${s.firewall.keyCount})` : ''}` : '—';
return `<tr>
<td>${idCell(s.swarmId, 18)}</td>
<td>${escapeHtml(s.appName || '—')}</td>
<td>${s.peerCount ?? s.connections?.length ?? 0}</td>
<td>${s.tabCount ?? 0}</td>
<td class="muted">${escapeHtml(fw)}</td>
<td class="time-ago">${s.createdAt ? timeAgo(s.createdAt) : '—'}</td>
<td><button type="button" class="btn btn-danger btn-sm" data-destroy="${escapeHtml(s.swarmId)}">Destroy</button></td>
</tr>`;
})
.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 = `<tr><td colspan="5"><div class="empty-state"><h4>No active connections</h4><p>Peer sockets show up after Hyperswarm discovers another peer on the same topic.</p></div></td></tr>`;
return;
}
tbody.innerHTML = connections
.map((c) => {
const topics = (c.topics || []).map((t) => truncate(t, 8)).join(', ') || '—';
return `<tr>
<td>${idCell(c.connId, 18)}</td>
<td>${idCell(c.swarmId, 12)}</td>
<td>${idCell(c.peerKey, 16)}</td>
<td class="mono muted">${escapeHtml(topics)}</td>
<td class="time-ago">${timeAgo(c.createdAt)}</td>
</tr>`;
})
.join('');
}
function updateTabsTable(state) {
const tbody = $('tabsTable');
const tabs = state.tabs || [];
if (!tabs.length) {
tbody.innerHTML = `<tr><td colspan="4" class="muted" style="padding:1rem">No BridgeSwarm tabs</td></tr>`;
return;
}
tbody.innerHTML = tabs
.map((t) => {
const title = t.title || `Tab ${t.tabId}`;
const url = t.url ? truncate(t.url, 40) : '';
return `<tr>
<td class="mono">${t.tabId}</td>
<td><div>${escapeHtml(title)}</div><div class="muted mono" style="font-size:0.7rem">${escapeHtml(url)}</div></td>
<td>${(t.swarmIds || []).map((id) => idCell(id, 10)).join(' ') || '—'}</td>
<td><button type="button" class="btn btn-ghost btn-sm" data-focus-tab="${t.tabId}">Focus</button></td>
</tr>`;
})
.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 `<span class="log-link" data-filter-id="${id}">${id}</span>`;
});
}
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 = '<div class="empty-state muted">No log events match filters</div>';
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 `<div class="log-row level-${level}" data-id="${escapeHtml(e.id || '')}">
<span class="log-time" title="${new Date(ts).toISOString()}">${time}</span>
<span class="log-level">${level}</span>
<span class="log-cat">${cat}</span>
<span class="log-msg">${linkifyMessage(e.message || '')}${meta ? `<div class="log-meta">${meta}</div>` : ''}</span>
</div>`;
})
.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 = '<div class="empty-state muted">No events yet</div>';
return;
}
el.innerHTML = recent
.map((e) => {
const ts = e.ts || e.timestamp;
return `<div class="log-row level-${e.level || 'info'}">
<span class="log-time">${new Date(ts).toLocaleTimeString()}</span>
<span class="log-msg">${escapeHtml(truncate(e.message, 80))}</span>
</div>`;
})
.join('');
}
/* ——— Settings ——— */
function renderSettingsForm() {
const root = $('settingsRoot');
if (!root) return;
const s = settings;
root.innerHTML = `
<div class="settings-group">
<h3>Notifications & debug</h3>
<div class="setting-row">
<div><div class="setting-label">Host disconnect notification</div><div class="setting-desc">Browser notification when the native host drops</div></div>
<div class="toggle" id="toggleNotify" data-key="notifyOnDisconnect"></div>
</div>
<div class="setting-row">
<div><div class="setting-label">Debug mode</div><div class="setting-desc">Verbose bridge logging in the Activity stream and page console</div></div>
<div class="toggle" id="toggleDebug" data-key="debug"></div>
</div>
<div class="setting-row">
<div><div class="setting-label">Disable on file:// URLs</div><div class="setting-desc">Skip injecting BridgeSwarm on local files</div></div>
<div class="toggle" id="toggleDisableFileUrls" data-key="disableOnFileUrls"></div>
</div>
</div>
<div class="settings-group">
<h3>Swarm defaults</h3>
<div class="setting-row">
<div><div class="setting-label">Default app name</div><div class="setting-desc">Used when a page omits appName</div></div>
<input type="text" id="defaultAppName" value="${escapeHtml(s.defaultAppName || '')}" style="width:200px">
</div>
<div class="setting-row">
<div><div class="setting-label">Default max peers</div><div class="setting-desc">0 = unlimited</div></div>
<input type="number" id="defaultMaxPeers" min="0" value="${s.defaultMaxPeers ?? 0}" style="width:100px">
</div>
<div class="setting-row">
<div><div class="setting-label">Request timeout (ms)</div><div class="setting-desc">Default BridgeSwarm.request timeout; 0 = none</div></div>
<input type="number" id="defaultRequestTimeoutMs" min="0" max="300000" value="${s.defaultRequestTimeoutMs ?? 0}" style="width:120px">
</div>
<div class="setting-row">
<div><div class="setting-label">Ready timeout (ms)</div><div class="setting-desc">BridgeSwarm.ready() timeout; 0 = wait forever</div></div>
<input type="number" id="readyTimeoutMs" min="0" max="60000" value="${s.readyTimeoutMs ?? 0}" style="width:120px">
</div>
<div class="setting-row">
<div><div class="setting-label">Capability job timeout (ms)</div><div class="setting-desc">Default wait for media/transcode jobs</div></div>
<input type="number" id="defaultCapJobTimeoutMs" min="1000" max="600000" value="${s.defaultCapJobTimeoutMs ?? 120000}" style="width:120px">
</div>
</div>
<div class="settings-group">
<h3>Default peer firewall</h3>
<div class="setting-row">
<div><div class="setting-label">Mode</div><div class="setting-desc">Applied automatically when a swarm initializes</div></div>
<select id="defaultFirewallMode">
<option value="off">Off</option>
<option value="allowlist">Allowlist</option>
<option value="denylist">Denylist</option>
</select>
</div>
<div class="setting-row">
<div><div class="setting-label">Peer keys</div><div class="setting-desc">One public-key hex per line</div></div>
<textarea id="defaultFirewallKeys" placeholder="abcdef…">${escapeHtml((s.defaultFirewallKeys || []).join('\n'))}</textarea>
</div>
</div>
<div class="settings-group">
<h3>Capability origins</h3>
<p class="muted" style="font-size:0.8rem;margin:0 0 0.75rem">
Packs (<code>fs</code>, <code>media</code>, <code>qvac</code>, <code>agent</code>, …) only run for listed origins.
The examples server origin is always included.
</p>
<div class="setting-row">
<div><div class="setting-label">Allowed origins</div><div class="setting-desc">One origin per line, e.g. https://app.example.com</div></div>
<textarea id="capabilityOrigins" placeholder="https://app.example.com">${escapeHtml((s.capabilityOrigins || []).join('\n'))}</textarea>
</div>
<div class="setting-row">
<div><div class="setting-label">Agent always-approve</div><div class="setting-desc">Origins that may skip agent permission prompts (trusted apps only)</div></div>
<textarea id="agentAlwaysApproveOrigins" placeholder="http://127.0.0.1:4173">${escapeHtml((s.agentAlwaysApproveOrigins || []).join('\n'))}</textarea>
</div>
<div class="setting-row">
<div><div class="setting-label">Agent workspace roots</div><div class="setting-desc">Absolute host paths the agent may read/write in addition to storage</div></div>
<textarea id="agentWorkspaceRoots" placeholder="/home/you/projects/my-app">${escapeHtml((s.agentWorkspaceRoots || []).join('\n'))}</textarea>
</div>
</div>
<div class="settings-group">
<h3>QVAC</h3>
<div class="setting-row">
<div><div class="setting-label">Enable QVAC</div><div class="setting-desc">Local on-device inference and the grok-class agent. Off until you turn this on.</div></div>
<div class="toggle" id="toggleQvacEnabled" data-key="qvacEnabled"></div>
</div>
</div>
<div class="settings-group">
<h3>Discord</h3>
<div class="setting-row">
<div><div class="setting-label">Enable Discord</div><div class="setting-desc">Run discord.js bots from the page via the Bare host (<code>BridgeSwarm.DiscordJS</code>). Off until you turn this on. Bot tokens are credentials.</div></div>
<div class="toggle" id="toggleDiscordEnabled" data-key="discordEnabled"></div>
</div>
</div>
<div class="settings-group">
<h3>Examples server</h3>
<div class="setting-row">
<div><div class="setting-label">Enable examples server</div><div class="setting-desc">Native host serves demos on localhost only</div></div>
<div class="toggle" id="toggleExamplesServer" data-key="examplesServerEnabled"></div>
</div>
<div class="setting-row">
<div><div class="setting-label">Port</div><div class="setting-desc">Bound to 127.0.0.1 only</div></div>
<input type="number" id="examplesServerPort" min="1024" max="65535" value="${s.examplesServerPort ?? 4173}" style="width:100px">
</div>
<div id="examplesServerPanel" class="examples-panel" style="display:none"></div>
</div>
<div class="settings-group">
<h3>Panel</h3>
<div class="setting-row">
<div><div class="setting-label">Refresh interval (ms)</div><div class="setting-desc">How often Overview polls getState (100010000)</div></div>
<input type="number" id="dashboardRefreshMs" min="1000" max="10000" step="500" value="${s.dashboardRefreshMs ?? 2000}" style="width:100px">
</div>
</div>
<div class="settings-group">
<h3>About</h3>
<p class="muted" style="font-size:0.8rem;margin:0 0 0.5rem">
BridgeSwarm — Hyperswarm in the browser via a Bare native messaging host.
</p>
<p class="muted" style="font-size:0.8rem;margin:0 0 0.5rem">
Version: <span class="mono" id="aboutVersion">—</span> · License: <span class="mono">AGPL-3.0</span>
</p>
<p class="muted" style="font-size:0.8rem;margin:0 0 0.5rem">
Owned and engineered by <strong style="color:var(--text)">HoneyPeer, LLC</strong>
(DeKalb County, Georgia, USA).
</p>
<p class="muted" style="font-size:0.8rem;margin:0">
Legal: <a href="mailto:[email protected]">[email protected]</a>
</p>
</div>
<p class="muted" style="font-size:0.75rem">Settings autosave. Reset restores defaults including examples server and QVAC off.</p>
`;
$('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);
$('toggleDiscordEnabled')?.classList.toggle('active', s.discordEnabled === 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 = `
<div class="mono" style="color:var(--accent);margin-bottom:0.5rem">${escapeHtml(url)}</div>
<div class="btn-row">
<button type="button" class="btn btn-primary btn-sm" id="btnOpenExamples">Open</button>
<button type="button" class="btn btn-secondary btn-sm" id="btnCopyExamplesUrl">Copy URL</button>
</div>`;
$('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 0300000', 'err');
return null;
}
if (isNaN(readyTimeout) || readyTimeout < 0 || readyTimeout > 60000) {
showToast('Ready timeout must be 060000', 'err');
return null;
}
if (isNaN(capTimeout) || capTimeout < 1000 || capTimeout > 600000) {
showToast('Cap job timeout must be 1000600000', 'err');
return null;
}
if (isNaN(port) || port < 1024 || port > 65535) {
showToast('Examples port must be 102465535', 'err');
return null;
}
if (isNaN(refresh) || refresh < 1000 || refresh > 10000) {
showToast('Refresh interval must be 100010000', '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,
discordEnabled: $('toggleDiscordEnabled')?.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');
$('capDiscordToggle')?.classList.toggle('active', settings.discordEnabled === true);
$('capDiscordToggle')?.setAttribute('aria-checked', settings.discordEnabled === 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 persistDiscordEnabled(enabled) {
const st = $('toggleDiscordEnabled');
if (st) st.classList.toggle('active', enabled);
const cap = $('capDiscordToggle');
if (cap) {
cap.classList.toggle('active', enabled);
cap.setAttribute('aria-checked', enabled ? 'true' : 'false');
}
settings.discordEnabled = enabled;
chrome.storage.local.set({ [SETTINGS_KEY]: { ...settings, discordEnabled: enabled } }, () => {
if (currentState) renderCapabilitiesPage(currentState);
showToast(enabled ? 'Discord enabled' : 'Discord disabled', 'ok');
});
}
$('capDiscordToggle')?.addEventListener('click', () => {
persistDiscordEnabled(settings.discordEnabled !== true);
});
$('capDiscordToggle')?.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();