This commit is contained in:
Raven Scott
2026-02-21 02:52:00 -05:00
parent 0e2e9edf94
commit 10785ab556
21 changed files with 1384 additions and 16 deletions
+3
View File
@@ -6,6 +6,7 @@
const btnLeave = document.getElementById('btnLeave');
const btnSend = document.getElementById('btnSend');
const peersEl = document.getElementById('peers');
const publicKeyEl = document.getElementById('publicKey');
const logEl = document.getElementById('log');
function setStatus(msg, isError) {
@@ -72,6 +73,8 @@
conn.on('error', (err) => log('Peer error: ' + err.message, 'err'));
});
await swarm.join(topic);
const pubKey = await swarm.getPublicKey();
publicKeyEl.textContent = 'Your public key: ' + pubKey;
setStatus('Joined "' + topic + '". Send a message or open this page in another tab.');
btnJoin.disabled = true;
btnLeave.disabled = false;
+1
View File
@@ -15,6 +15,7 @@
<button class="danger" id="btnLeave" disabled>Leave</button>
</div>
<div class="peers" id="peers">Peers: 0</div>
<div class="peers" id="publicKey">Your public key: (join to generate)</div>
<div class="row">
<input type="text" id="message" placeholder="Type a message..." disabled>
<button class="primary" id="btnSend" disabled>Send</button>
+5 -1
View File
@@ -24,7 +24,11 @@
setTimeout(function () { initWhenReady(attempts + 1); }, 100);
return;
}
window.BridgeSwarm.ready().then(function () {
window.BridgeSwarm.ready().then(async function () {
const swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-data' });
const pubKey = await swarm.getPublicKey();
document.getElementById('publicKey').textContent = 'Your public key: ' + pubKey;
await swarm.destroy();
log('Data API ready. Use the buttons above.');
document.getElementById('coreInfo').onclick = async function () {
+5
View File
@@ -9,6 +9,11 @@
<body>
<h1>BridgeSwarm Data API Demo</h1>
<p>Uses <code>BridgeSwarm.request(type, payload)</code> to call Hypercore, Hyperbee, Hyperdrive, Autobase, and Hyperdb in the native host.</p>
<div class="section">
<div class="row">
<span id="publicKey">Your public key: (loading...)</span>
</div>
</div>
<div class="section">
<h2>Hypercore</h2>
+3
View File
@@ -4,6 +4,7 @@
const btnJoin = document.getElementById('btnJoin');
const btnLeave = document.getElementById('btnLeave');
const peersEl = document.getElementById('peers');
const publicKeyEl = document.getElementById('publicKey');
const connSelectRow = document.getElementById('connSelectRow');
const connSelect = document.getElementById('connSelect');
const btnEnableHrpc = document.getElementById('btnEnableHrpc');
@@ -150,6 +151,8 @@
});
await swarm.join(topic);
const pubKey = await swarm.getPublicKey();
publicKeyEl.textContent = 'Your public key: ' + pubKey;
setStatus('Joined "' + topic + '". Open another tab and join the same topic; HRPC enables automatically.', 'ok');
btnJoin.disabled = true;
btnLeave.disabled = false;
+1
View File
@@ -23,6 +23,7 @@
<div class="section">
<h2>2. Connection &amp; HRPC <span class="badge">attachHrpc, hrpcInvoke</span></h2>
<p id="peers">Peers: 0</p>
<p id="publicKey">Your public key: (join to generate)</p>
<div class="row" id="connSelectRow" style="display:none;">
<label for="connSelect" style="font-size:0.85rem; color:#a9b1d6;">Use connection:</label>
<select id="connSelect" style="min-width:180px; padding:0.35rem 0.5rem; border-radius:6px; border:1px solid #3b4261; background:#24283b; color:#c0caf5; font-size:0.85rem;"></select>
+3
View File
@@ -6,6 +6,7 @@
const btnShare = document.getElementById('btnShare');
const btnStopShare = document.getElementById('btnStopShare');
const peersEl = document.getElementById('peers');
const publicKeyEl = document.getElementById('publicKey');
const remoteVideo = document.getElementById('remoteVideo');
const videoPlaceholder = document.getElementById('videoPlaceholder');
const placeholderBlock = document.getElementById('placeholderBlock');
@@ -267,6 +268,8 @@
conn.on('error', function (err) { console.warn('Peer error:', err); });
});
await swarm.join(topic);
const pubKey = await swarm.getPublicKey();
publicKeyEl.textContent = 'Your public key: ' + pubKey;
setStatus('Joined. Share your screen now or wait for viewers.');
btnJoin.disabled = true;
btnLeave.disabled = false;
+1
View File
@@ -15,6 +15,7 @@
<button class="danger" id="btnLeave" disabled>Leave</button>
</div>
<div class="peers" id="peers">Peers: 0</div>
<div class="peers" id="publicKey">Your public key: (join to generate)</div>
<div class="row">
<button class="primary" id="btnShare" disabled>Share my screen</button>
<button class="secondary" id="btnStopShare" disabled style="display: none;">Stop sharing</button>
+3
View File
@@ -4,6 +4,7 @@
const btnJoin = document.getElementById('btnJoin');
const btnLeave = document.getElementById('btnLeave');
const peersEl = document.getElementById('peers');
const publicKeyEl = document.getElementById('publicKey');
const connStatusEl = document.getElementById('connStatus');
const rawMsgEl = document.getElementById('rawMsg');
const btnRawSend = document.getElementById('btnRawSend');
@@ -125,6 +126,8 @@
});
await swarm.join(topic);
const pubKey = await swarm.getPublicKey();
publicKeyEl.textContent = 'Your public key: ' + pubKey;
setStatus('Joined "' + topic + '". Connect another tab to try raw and Protomux messages.');
btnJoin.disabled = true;
btnLeave.disabled = false;
+1
View File
@@ -23,6 +23,7 @@
<div class="section">
<h2>2. Connections <span class="badge">peerInfo, connection events</span></h2>
<p id="peers">Peers: 0</p>
<p id="publicKey">Your public key: (join to generate)</p>
<p class="status ok" id="connStatus" style="display:none;">On connection you get (conn, peerInfo). peerInfo: publicKey (hex), topics (hex[]).</p>
</div>
+3
View File
@@ -5,6 +5,7 @@
const btnLeave = document.getElementById('btnLeave');
const btnClear = document.getElementById('btnClear');
const peersEl = document.getElementById('peers');
const publicKeyEl = document.getElementById('publicKey');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
@@ -202,6 +203,8 @@
conn.on('error', () => {});
});
await swarm.join(topic);
const pubKey = await swarm.getPublicKey();
publicKeyEl.textContent = 'Your public key: ' + pubKey;
setStatus('Joined "' + topic + '". Draw on the canvas; open in another tab to collaborate.');
btnJoin.disabled = true;
btnLeave.disabled = false;
+1
View File
@@ -16,6 +16,7 @@
<button class="secondary" id="btnClear" disabled>Clear board</button>
</div>
<div class="peers" id="peers">Peers: 0</div>
<div class="peers" id="publicKey">Your public key: (join to generate)</div>
<div class="canvas-wrap">
<canvas id="canvas" width="800" height="500"></canvas>
</div>
+25 -1
View File
@@ -113,7 +113,8 @@
defaults.maxPeers != null && defaults.maxPeers > 0 && opts.maxPeers == null && { maxPeers: defaults.maxPeers },
opts
);
this.swarmId = 'swarm_' + generateId();
// Use appName as swarmId so multiple tabs share the same swarm
this.swarmId = 'swarm_' + (this.options.appName || 'default');
this._initPromise = null;
this._eventUnsub = null;
this._boundOnEvent = this._onEvent.bind(this);
@@ -129,6 +130,11 @@
payload: { swarmId: this.swarmId, options: this.options },
}).then((res) => {
if (res && !res.ok) throw new Error(res.error || 'Init failed');
sendToBridge({
id: generateId(),
type: 'registerSwarm',
payload: { swarmId: this.swarmId },
}).catch(() => {});
this._listenEvents();
return res;
});
@@ -147,6 +153,7 @@
const msg = e.detail;
if (!msg || msg.type !== 'event') return;
const payload = msg.payload || {};
console.log('[BridgeSwarm-api] Event received:', msg.event, 'connId:', payload.connId, 'swarmId:', payload.swarmId);
if (payload.swarmId !== this.swarmId) return;
if (msg.event === 'connection') {
@@ -155,6 +162,7 @@
this._connections.set(payload.connId, conn);
conn.on('end', () => { if (this._connections) this._connections.delete(payload.connId); });
const peerInfo = payload.peerInfo || {};
console.log('[BridgeSwarm-api] Emitting connection event for connId:', payload.connId);
this.emit('connection', conn, peerInfo);
return;
}
@@ -167,6 +175,7 @@
return;
}
if (msg.event === 'end' || msg.event === 'error') {
console.log('[BridgeSwarm-api] END event for connId:', payload.connId);
const conn = this._connections && this._connections.get(payload.connId);
if (conn) {
conn.destroyed = true;
@@ -213,6 +222,21 @@
});
};
/**
* Get the public key (hex) of this swarm's key pair.
* @returns {Promise<string>} Public key as 64-character hex string.
*/
BridgeSwarm.prototype.getPublicKey = function () {
return sendToBridge({
id: generateId(),
type: 'swarmKey',
payload: { swarmId: this.swarmId },
}).then((res) => {
if (res && !res.ok) throw new Error(res.error || 'Failed to get public key');
return res.publicKey;
});
};
/**
* Snapshot of current connection objects. After destroy() returns [].
* @returns {BridgeSwarmConnection[]}
+200 -1
View File
@@ -7,6 +7,29 @@ const HOST_NAME = 'com.bridgeswarm';
const MAX_RECONNECT_DELAY = 30000;
const INITIAL_RECONNECT_DELAY = 100;
function log(...args) {
console.log('[BridgeSwarm-bg]', ...args);
// Store logs for dashboard
const timestamp = Date.now();
const message = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
logs.push({ timestamp, message, level: 'info' });
if (logs.length > MAX_LOGS) logs.shift();
broadcastLogs();
}
const logs = [];
const MAX_LOGS = 500;
function broadcastLogs() {
// Send recent logs to any dashboard that's open
for (const tabId of dashboardTabs) {
browser.tabs.sendMessage(tabId, { type: 'bridge-swarm-logs', logs: logs.slice(-50) }).catch(() => {});
}
}
// Track dashboard tabs
const dashboardTabs = new Set();
const browser = typeof chrome !== 'undefined' && chrome.runtime?.connectNative
? chrome
: typeof browser !== 'undefined'
@@ -23,9 +46,52 @@ const pending = new Map();
// Tabs that have subscribed to native host events (content script registered)
const subscribedTabs = new Set();
// Track which swarmIds belong to which tab: tabId -> Set<swarmId>
const tabSwarms = new Map();
// Track how many tabs are using each swarm: swarmId -> count
const swarmRefCount = new Map();
// Track active connections from native host: connId -> { swarmId, peerKey, createdAt }
const activeConnections = new Map();
const SETTINGS_KEY = 'bridgeSwarmSettings';
let notifyOnDisconnect = false;
// Extension state for the UI
const extensionState = {
hostConnected: false,
swarms: new Map(), // swarmId -> { appName, tabCount, connections: [], createdAt }
stats: {
totalConnections: 0,
totalSwarms: 0,
uptime: Date.now()
}
};
function updateExtensionState() {
extensionState.hostConnected = !!port;
extensionState.stats.totalConnections = activeConnections.size;
// Update swarms from tabSwarms
extensionState.swarms.clear();
for (const [swarmId, count] of swarmRefCount) {
const connections = [];
for (const [connId, conn] of activeConnections) {
if (conn.swarmId === swarmId) {
connections.push(conn);
}
}
extensionState.swarms.set(swarmId, {
swarmId,
appName: swarmId.replace('swarm_', ''),
tabCount: count,
connections,
createdAt: Date.now()
});
}
extensionState.stats.totalSwarms = extensionState.swarms.size;
}
function loadNotifySetting() {
browser.storage.local.get(SETTINGS_KEY, (result) => {
const s = result[SETTINGS_KEY] || {};
@@ -43,8 +109,9 @@ browser.storage.onChanged.addListener((changes, areaName) => {
function connect() {
try {
port = browser.runtime.connectNative(HOST_NAME);
log('Connected to native host');
} catch (e) {
console.error('[BridgeSwarm] connectNative failed:', e);
log('connectNative failed:', e);
scheduleReconnect();
return;
}
@@ -52,6 +119,37 @@ function connect() {
reconnectDelay = INITIAL_RECONNECT_DELAY;
port.onMessage.addListener((msg) => {
log('Received from native:', msg.type, msg.event || '');
// Track connection events
if (msg.type === 'event') {
const payload = msg.payload || {};
if (msg.event === 'connection') {
const connId = payload.connId;
const swarmId = payload.swarmId;
const peerInfo = payload.peerInfo || {};
log('Connection established:', connId, 'peer:', peerInfo.publicKey?.slice(0, 8));
activeConnections.set(connId, {
connId,
swarmId,
peerKey: peerInfo.publicKey || '',
createdAt: Date.now()
});
updateExtensionState();
} else if (msg.event === 'error') {
const connId = payload.connId;
const errorMsg = payload.message || 'Unknown error';
log('Connection ERROR:', connId, errorMsg);
activeConnections.delete(connId);
updateExtensionState();
} else if (msg.event === 'end') {
const connId = payload.connId;
log('Connection ended:', connId);
activeConnections.delete(connId);
updateExtensionState();
}
}
if (msg.type === 'response' && msg.id != null) {
const p = pending.get(msg.id);
if (p) {
@@ -65,6 +163,7 @@ function connect() {
return;
}
if (msg.type === 'event') {
log('Broadcasting event to tabs:', msg.event, 'connId:', msg.payload?.connId);
for (const tabId of subscribedTabs) {
browser.tabs.sendMessage(tabId, { type: 'bridge-swarm-event', payload: msg }).catch(() => {});
}
@@ -72,6 +171,10 @@ function connect() {
});
port.onDisconnect.addListener(() => {
log('Native host disconnected');
extensionState.hostConnected = false;
activeConnections.clear();
updateExtensionState();
port = null;
if (notifyOnDisconnect && browser.notifications) {
browser.notifications.create('bridgeswarm-host-disconnect', {
@@ -103,6 +206,7 @@ function scheduleReconnect() {
}
function send(msg) {
log('Sending to native:', msg.type, msg.payload?.swarmId || '', msg.payload?.connId || '');
return new Promise((resolve, reject) => {
if (!port) {
reject(new Error('Native host not connected'));
@@ -123,11 +227,50 @@ function send(msg) {
loadNotifySetting();
connect();
// Handle tab closure - destroy swarms only when last tab using them closes
browser.tabs.onRemoved.addListener((tabId) => {
log('Tab closed:', tabId);
const swarmIds = tabSwarms.get(tabId);
if (swarmIds) {
for (const swarmId of swarmIds) {
const count = (swarmRefCount.get(swarmId) || 1) - 1;
swarmRefCount.set(swarmId, count);
log('Swarm', swarmId, 'refcount now:', count);
// Only destroy when last tab closes
if (count <= 0) {
log('Last tab for swarm', swarmId, '- destroying');
send({ type: 'destroy', payload: { swarmId } }).catch(() => {});
swarmRefCount.delete(swarmId);
}
}
tabSwarms.delete(tabId);
}
subscribedTabs.delete(tabId);
});
// Content script talks to background via runtime.sendMessage / onMessage
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.target !== 'bridge-swarm-native') {
return;
}
// Handle registerSwarm locally - don't forward to native host
if (message.action === 'send' && message.payload?.type === 'registerSwarm') {
const swarmId = message.payload?.payload?.swarmId;
if (swarmId && sender.tab?.id != null) {
if (!tabSwarms.has(sender.tab.id)) {
tabSwarms.set(sender.tab.id, new Set());
}
tabSwarms.get(sender.tab.id).add(swarmId);
// Increment reference count
const count = swarmRefCount.get(swarmId) || 0;
swarmRefCount.set(swarmId, count + 1);
log('Registered swarm', swarmId, 'for tab', sender.tab.id, '- refcount:', count + 1);
}
sendResponse({ ok: true });
return false;
}
if (message.action === 'send') {
send(message.payload)
.then(sendResponse)
@@ -144,4 +287,60 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse({ ok: true });
return false;
}
// Handle dashboard registration
if (message.action === 'registerDashboard' && sender.tab && sender.tab.id != null) {
dashboardTabs.add(sender.tab.id);
log('Dashboard registered for tab', sender.tab.id);
// Send current logs immediately
sendResponse({ ok: true, logs: logs.slice(-50) });
return false;
}
if (message.action === 'unregisterDashboard' && sender.tab && sender.tab.id != null) {
dashboardTabs.delete(sender.tab.id);
sendResponse({ ok: true });
return false;
}
// Handle getState request from UI
if (message.action === 'getState') {
updateExtensionState();
// Build tabs info
const tabsInfo = [];
for (const [tabId, swarmIds] of tabSwarms) {
tabsInfo.push({
tabId,
swarmIds: Array.from(swarmIds)
});
}
sendResponse({
ok: true,
state: {
hostConnected: extensionState.hostConnected,
swarms: Array.from(extensionState.swarms.values()),
tabs: tabsInfo,
stats: {
totalConnections: extensionState.stats.totalConnections,
totalSwarms: extensionState.stats.totalSwarms,
uptime: extensionState.stats.uptime
}
}
});
return false;
}
// Handle destroySwarm request from UI
if (message.action === 'destroySwarm' && message.payload?.swarmId) {
const swarmId = message.payload.swarmId;
send({ type: 'destroy', payload: { swarmId } })
.then(sendResponse)
.catch((err) => sendResponse({ ok: false, error: err.message }));
return true;
}
});
// Open dashboard when extension icon is clicked
browser.action?.onClicked?.addListener((tab) => {
browser.tabs.create({ url: 'dashboard.html' });
});
+17 -10
View File
@@ -7,9 +7,13 @@
const browser = typeof chrome !== 'undefined' && chrome.runtime?.sendMessage ? chrome : typeof browser !== 'undefined' ? browser : chrome;
const SETTINGS_KEY = 'bridgeSwarmSettings';
let debugMode = false;
let debugMode = true; // Always on for debugging
const DEFAULTS = { defaultAppName: 'bridge-swarm', defaultRequestTimeoutMs: 0, defaultMaxPeers: 0, readyTimeoutMs: 0, disableOnFileUrls: false, debug: false };
function log(...args) {
console.log('[BridgeSwarm-content]', ...args);
}
function injectApi() {
function inject(src, next) {
const script = document.createElement('script');
@@ -32,12 +36,14 @@ function injectApi() {
const readyTimeoutMs = typeof s.readyTimeoutMs === 'number' ? s.readyTimeoutMs : DEFAULTS.readyTimeoutMs;
const debug = s.debug === true;
debugMode = debug;
const defaultsScript = document.createElement('script');
defaultsScript.textContent =
'window.__BRIDGESWARM_DEFAULTS__={appName:' + JSON.stringify(appName) + ',requestTimeoutMs:' + requestTimeoutMs + ',maxPeers:' + maxPeers + ',readyTimeoutMs:' + readyTimeoutMs + '};' +
'window.__BRIDGESWARM_DEBUG__=' + (debug ? 'true' : 'false') + ';';
(document.head || document.documentElement).appendChild(defaultsScript);
defaultsScript.remove();
const defaults = {
appName: appName,
requestTimeoutMs: requestTimeoutMs,
maxPeers: maxPeers,
readyTimeoutMs: readyTimeoutMs
};
window.__BRIDGESWARM_DEFAULTS__ = defaults;
window.__BRIDGESWARM_DEBUG__ = debug;
inject('api.js', () => inject('framed-stream.js', () => inject('protomux-bundle.js')));
});
}
@@ -50,9 +56,11 @@ window.addEventListener('message', (event) => {
if (event.data?.type !== 'bridge-swarm-bridge') return;
const { id, action, payload } = event.data;
log('Page -> Background:', action, payload?.type || '');
browser.runtime.sendMessage(
{ target: 'bridge-swarm-native', action, payload },
(response) => {
log('Background -> Page response:', id, response?.error || 'ok');
const err = response?.error;
window.dispatchEvent(
new CustomEvent('bridge-swarm-bridge-response', {
@@ -65,16 +73,15 @@ window.addEventListener('message', (event) => {
// Subscribe to native host events once (from content script context)
browser.runtime.sendMessage({ target: 'bridge-swarm-native', action: 'subscribe' }, () => {
if (debugMode) console.log('[BridgeSwarm] Subscribed to native host events');
log('Subscribed to native host events');
});
browser.runtime.onMessage.addListener((message) => {
log('Received from background:', message.type, message.payload?.event || '', message.payload?.connId || '');
if (message.type === 'bridge-swarm-event') {
if (debugMode) console.log('[BridgeSwarm] event', message.payload);
window.dispatchEvent(new CustomEvent('bridge-swarm-event', { detail: message.payload }));
}
if (message.type === 'bridge-swarm-host-disconnect') {
if (debugMode) console.log('[BridgeSwarm] host disconnected');
window.dispatchEvent(new CustomEvent('bridge-swarm-host-disconnect'));
}
});
+677
View File
@@ -0,0 +1,677 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BridgeSwarm Dashboard</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f14;
color: #e4e4e7;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 240px;
background: #18181b;
border-right: 1px solid #27272a;
padding: 1.5rem 0;
display: flex;
flex-direction: column;
}
.logo {
padding: 0 1.5rem 1.5rem;
border-bottom: 1px solid #27272a;
margin-bottom: 1rem;
}
.logo h1 {
font-size: 1.25rem;
font-weight: 700;
color: #fff;
display: flex;
align-items: center;
gap: 0.5rem;
}
.logo h1 span {
color: #22d3ee;
}
.logo .version {
font-size: 0.75rem;
color: #71717a;
margin-top: 0.25rem;
}
.nav {
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1.5rem;
color: #a1a1aa;
text-decoration: none;
font-size: 0.9rem;
transition: all 0.15s;
cursor: pointer;
}
.nav-item:hover {
background: #27272a;
color: #e4e4e7;
}
.nav-item.active {
background: #22d3ee15;
color: #22d3ee;
border-right: 2px solid #22d3ee;
}
.nav-item svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.nav-item .badge {
margin-left: auto;
background: #27272a;
padding: 0.125rem 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
}
.nav-item.active .badge {
background: #22d3ee25;
color: #22d3ee;
}
/* Main content */
.main {
margin-left: 240px;
padding: 2rem;
}
.page {
display: none;
}
.page.active {
display: block;
}
/* Status banner */
.status-banner {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.25rem;
border-radius: 8px;
margin-bottom: 2rem;
font-size: 0.9rem;
}
.status-banner.connected {
background: #22d3ee15;
border: 1px solid #22d3ee35;
}
.status-banner.disconnected {
background: #f43f5e15;
border: 1px solid #f43f5e35;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.connected .status-dot { background: #22d3ee; }
.disconnected .status-dot { background: #f43f5e; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Cards */
.card {
background: #18181b;
border: 1px solid #27272a;
border-radius: 12px;
overflow: hidden;
margin-bottom: 1.5rem;
}
.card-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid #27272a;
display: flex;
align-items: center;
justify-content: space-between;
}
.card-title {
font-size: 0.95rem;
font-weight: 600;
color: #fafafa;
}
.card-body {
padding: 1rem 1.25rem;
}
/* Stats grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: #18181b;
border: 1px solid #27272a;
border-radius: 12px;
padding: 1.25rem;
}
.stat-label {
font-size: 0.8rem;
color: #71717a;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #fff;
}
.stat-value.accent { color: #22d3ee; }
.stat-value.warning { color: #fbbf24; }
.stat-value.danger { color: #f43f5e; }
/* Tables */
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #27272a;
}
.table th {
font-size: 0.75rem;
font-weight: 600;
color: #71717a;
text-transform: uppercase;
letter-spacing: 0.05em;
background: #1f1f23;
}
.table td {
font-size: 0.875rem;
color: #a1a1aa;
}
.table tr:hover td {
background: #27272a;
}
.table .mono {
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
font-size: 0.8rem;
color: #22d3ee;
}
.table .empty {
text-align: center;
color: #52525b;
padding: 2rem;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.15s;
}
.btn-primary {
background: #22d3ee;
color: #0f0f14;
}
.btn-primary:hover {
background: #06b6d4;
}
.btn-danger {
background: #f43f5e20;
color: #f43f5e;
border: 1px solid #f43f5e35;
}
.btn-danger:hover {
background: #f43f5e35;
}
.btn-sm {
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Tags */
.tag {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.tag-success {
background: #22d3ee20;
color: #22d3ee;
}
.tag-warning {
background: #fbbf2420;
color: #fbbf24;
}
.tag-error {
background: #f43f5e20;
color: #f43f5e;
}
/* Time ago */
.time-ago {
color: #52525b;
font-size: 0.8rem;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #18181b;
}
::-webkit-scrollbar-thumb {
background: #3f3f46;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #52525b;
}
/* Refresh button */
.refresh-btn {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 48px;
height: 48px;
border-radius: 50%;
background: #22d3ee;
color: #0f0f14;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(34, 211, 238, 0.3);
transition: all 0.2s;
}
.refresh-btn:hover {
transform: rotate(180deg);
}
.refresh-btn svg {
width: 24px;
height: 24px;
}
/* Settings section */
.settings-group {
margin-bottom: 2rem;
}
.settings-group h3 {
font-size: 1rem;
font-weight: 600;
color: #fafafa;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #27272a;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #27272a;
}
.setting-row:last-child {
border-bottom: none;
}
.setting-info {
flex: 1;
}
.setting-label {
font-size: 0.9rem;
color: #e4e4e7;
}
.setting-desc {
font-size: 0.8rem;
color: #71717a;
margin-top: 0.25rem;
}
.toggle {
position: relative;
width: 44px;
height: 24px;
background: #3f3f46;
border-radius: 12px;
cursor: pointer;
transition: background 0.2s;
}
.toggle.active {
background: #22d3ee;
}
.toggle::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.toggle.active::after {
transform: translateX(20px);
}
/* Logs console */
.logs-container {
background: #0d0d0f;
border: 1px solid #27272a;
border-radius: 8px;
height: calc(100vh - 200px);
overflow-y: auto;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
font-size: 0.8rem;
}
.log-entry {
padding: 0.375rem 0.75rem;
border-bottom: 1px solid #1f1f23;
display: flex;
gap: 0.75rem;
}
.log-entry:hover {
background: #18181b;
}
.log-time {
color: #52525b;
flex-shrink: 0;
}
.log-msg {
color: #a1a1aa;
word-break: break-all;
}
.logs-toolbar {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.logs-toolbar .btn {
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
}
.log-level-info { color: #22d3ee; }
.log-level-warn { color: #fbbf24; }
.log-level-error { color: #f43f5e; }
</style>
</head>
<body>
<!-- Sidebar -->
<nav class="sidebar">
<div class="logo">
<h1>🦅 <span>BridgeSwarm</span></h1>
<div class="version">v1.0.0</div>
</div>
<div class="nav">
<div class="nav-item active" data-page="dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
Dashboard
</div>
<div class="nav-item" data-page="swarms">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/><circle cx="19" cy="5" r="2"/><circle cx="5" cy="19" r="2"/>
<circle cx="5" cy="5" r="2"/><circle cx="19" cy="19" r="2"/>
<line x1="12" y1="9" x2="12" y2="5"/><line x1="9.5" y1="14.5" x2="6" y2="17"/>
<line x1="14.5" y1="14.5" x2="18" y2="17"/>
</svg>
Swarms
<span class="badge" id="swarmCount">0</span>
</div>
<div class="nav-item" data-page="connections">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Connections
<span class="badge" id="connCount">0</span>
</div>
<div class="nav-item" data-page="tabs">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/>
</svg>
Tabs
</div>
<div class="nav-item" data-page="logs">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/><polyline points="10,9 9,9 8,9"/>
</svg>
Logs
<span class="badge" id="logCount">0</span>
</div>
<div class="nav-item" data-page="settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
Settings
</div>
</div>
</nav>
<!-- Main Content -->
<main class="main">
<!-- Dashboard Page -->
<div class="page active" id="page-dashboard">
<div class="status-banner" id="statusBanner">
<div class="status-dot"></div>
<span id="statusText">Connecting...</span>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Active Swarms</div>
<div class="stat-value accent" id="dashSwarms">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Active Connections</div>
<div class="stat-value" id="dashConnections">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Active Tabs</div>
<div class="stat-value" id="dashTabs">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Uptime</div>
<div class="stat-value" id="dashUptime">0s</div>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Recent Connections</span>
</div>
<div class="card-body" style="padding: 0;">
<table class="table">
<thead>
<tr>
<th>Connection ID</th>
<th>Swarm</th>
<th>Peer Key</th>
<th>Connected</th>
</tr>
</thead>
<tbody id="recentConnections">
<tr><td colspan="4" class="empty">No active connections</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Swarms Page -->
<div class="page" id="page-swarms">
<h2 style="margin-bottom: 1.5rem; font-size: 1.25rem;">Active Swarms</h2>
<div class="card">
<div class="card-body" style="padding: 0;">
<table class="table">
<thead>
<tr>
<th>Swarm ID</th>
<th>App Name</th>
<th>Connections</th>
<th>Tab Count</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="swarmsTable">
<tr><td colspan="5" class="empty">No active swarms</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Connections Page -->
<div class="page" id="page-connections">
<h2 style="margin-bottom: 1.5rem; font-size: 1.25rem;">Active Connections</h2>
<div class="card">
<div class="card-body" style="padding: 0;">
<table class="table">
<thead>
<tr>
<th>Connection ID</th>
<th>Swarm</th>
<th>Peer Key</th>
<th>Connected</th>
</tr>
</thead>
<tbody id="connectionsTable">
<tr><td colspan="4" class="empty">No active connections</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Tabs Page -->
<div class="page" id="page-tabs">
<h2 style="margin-bottom: 1.5rem; font-size: 1.25rem;">Active Tabs</h2>
<div class="card">
<div class="card-body" style="padding: 0;">
<table class="table">
<thead>
<tr>
<th>Tab ID</th>
<th>Swarm IDs</th>
</tr>
</thead>
<tbody id="tabsTable">
<tr><td colspan="2" class="empty">No active tabs</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Settings Page -->
<div class="page" id="page-settings">
<h2 style="margin-bottom: 1.5rem; font-size: 1.25rem;">Extension Settings</h2>
<div class="card">
<div class="card-body">
<div class="settings-group">
<h3>Notifications</h3>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Host Disconnect</div>
<div class="setting-desc">Show notification when native host disconnects</div>
</div>
<div class="toggle" id="toggleNotify" data-setting="notifyOnDisconnect"></div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Debug Mode</div>
<div class="setting-desc">Log events to console for troubleshooting</div>
</div>
<div class="toggle" id="toggleDebug" data-setting="debug"></div>
</div>
</div>
<div class="settings-group">
<h3>Defaults</h3>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Default App Name</div>
<div class="setting-desc">Used when page doesn't specify appName</div>
</div>
<input type="text" id="defaultAppName" style="width: 200px; padding: 0.5rem; background: #27272a; border: 1px solid #3f3f46; border-radius: 6px; color: #e4e4e7;">
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Default Max Peers</div>
<div class="setting-desc">Maximum peer connections per swarm</div>
</div>
<input type="number" id="defaultMaxPeers" style="width: 100px; padding: 0.5rem; background: #27272a; border: 1px solid #3f3f46; border-radius: 6px; color: #e4e4e7;">
</div>
</div>
<div style="margin-top: 1.5rem;">
<button class="btn btn-primary" id="btnSaveSettings">Save Settings</button>
</div>
</div>
</div>
</div>
<!-- Logs Page -->
<div class="page" id="page-logs">
<h2 style="margin-bottom: 1rem; font-size: 1.25rem;">System Logs</h2>
<div class="logs-toolbar">
<button class="btn btn-primary" id="btnClearLogs">Clear Logs</button>
<button class="btn btn-secondary" id="btnAutoScroll">Auto-scroll: ON</button>
</div>
<div class="logs-container" id="logsContainer">
<div class="empty" style="padding: 2rem; text-align: center; color: #52525b;">No logs yet</div>
</div>
</div>
</main>
<!-- Refresh Button -->
<button class="refresh-btn" id="refreshBtn" title="Refresh">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23,4 23,10 17,10"/><polyline points="1,20 1,14 7,14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
</button>
<script src="dashboard.js"></script>
</body>
</html>
+353
View File
@@ -0,0 +1,353 @@
/**
* 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);
$('defaultAppName').value = settings.defaultAppName || 'bridge-swarm';
$('defaultMaxPeers').value = settings.defaultMaxPeers || '';
}
// Save settings
function saveSettings() {
settings.notifyOnDisconnect = $('toggleNotify').classList.contains('active');
settings.debug = $('toggleDebug').classList.contains('active');
settings.defaultAppName = $('defaultAppName').value || 'bridge-swarm';
settings.defaultMaxPeers = parseInt($('defaultMaxPeers').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;
// 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 = '<tr><td colspan="4" class="empty">No active connections</td></tr>';
} else {
recentConnBody.innerHTML = connections.slice(0, 10).map(conn => `
<tr>
<td class="mono">${truncate(conn.connId)}</td>
<td>${truncate(conn.swarmId, 12)}</td>
<td class="mono">${truncate(conn.peerKey, 12)}</td>
<td class="time-ago">${timeAgo(conn.createdAt)}</td>
</tr>
`).join('');
}
}
// Update swarms table
function updateSwarmsTable(state) {
const tbody = $('swarmsTable');
if (state.swarms.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty">No active swarms</td></tr>';
return;
}
tbody.innerHTML = state.swarms.map(swarm => `
<tr>
<td class="mono">${truncate(swarm.swarmId, 20)}</td>
<td>${swarm.appName}</td>
<td>${swarm.connections.length}</td>
<td>${swarm.tabCount}</td>
<td>
<button class="btn btn-danger btn-sm" onclick="destroySwarm('${swarm.swarmId}')">Destroy</button>
</td>
</tr>
`).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 = '<tr><td colspan="4" class="empty">No active connections</td></tr>';
return;
}
tbody.innerHTML = connections.map(conn => `
<tr>
<td class="mono">${truncate(conn.connId, 20)}</td>
<td>${truncate(conn.swarmId, 16)}</td>
<td class="mono">${truncate(conn.peerKey, 16)}</td>
<td class="time-ago">${timeAgo(conn.createdAt)}</td>
</tr>
`).join('');
}
// Update tabs table
function updateTabsTable(state) {
const tbody = $('tabsTable');
if (state.tabs.length === 0) {
tbody.innerHTML = '<tr><td colspan="2" class="empty">No active tabs</td></tr>';
return;
}
tbody.innerHTML = state.tabs.map(tab => `
<tr>
<td>${tab.tabId}</td>
<td>${tab.swarmIds.map(id => `<span class="mono">${truncate(id, 12)}</span>`).join(', ') || 'none'}</td>
</tr>
`).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;
$('logCount').textContent = logs.length;
if (logs.length === 0) {
container.innerHTML = '<div class="empty" style="padding: 2rem; text-align: center; color: #52525b;">No logs yet</div>';
return;
}
container.innerHTML = logs.map(entry => {
const time = new Date(entry.timestamp).toLocaleTimeString();
return `<div class="log-entry">
<span class="log-time">${time}</span>
<span class="log-msg">${escapeHtml(entry.message)}</span>
</div>`;
}).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();
+9
View File
@@ -0,0 +1,9 @@
/**
* Default settings injected into the page before api.js loads.
*/
(function () {
var key = '__BRIDGESWARM_DEFAULTS__';
if (typeof window[key] === 'undefined') {
window[key] = {};
}
})();
+9 -2
View File
@@ -8,8 +8,12 @@
"nativeMessaging",
"storage",
"scripting",
"notifications"
"notifications",
"tabs"
],
"action": {
"default_title": "BridgeSwarm Dashboard"
},
"background": {
"service_worker": "background.js"
},
@@ -29,7 +33,10 @@
"resources": [
"api.js",
"framed-stream.js",
"protomux-bundle.js"
"protomux-bundle.js",
"defaults.js",
"dashboard.html",
"dashboard.js"
],
"matches": [
"<all_urls>"
+10
View File
@@ -80,6 +80,16 @@
<body>
<h1>BridgeSwarm</h1>
<p class="sub">Extension settings. Changes apply to new swarms and requests.</p>
<p style="margin-bottom: 1.5rem;">
<a href="dashboard.html" target="_blank" style="color: #7aa2f7; text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
Open Dashboard →
</a>
</p>
<section>
<h2>Swarm defaults</h2>
+54 -1
View File
@@ -12,12 +12,40 @@ const Hyperdrive = require('hyperdrive');
const Autobase = require('autobase');
const HyperDB = require('hyperdb');
const path = require('bare-path');
const fs = require('bare-fs');
const { Duplex } = require('bare-stream');
const c = require('compact-encoding');
const def = require(path.join(path.dirname(require.resolve('hyperdb')), 'lib', 'definition.js'));
const minimalDefinition = require('./hyperdb-minimal-definition.js');
const b4a = require('b4a');
// File-based logging
const LOG_FILE = path.join(__dirname, 'bridge-swarm.log');
let logStream = null;
function getLogStream() {
if (!logStream) {
try {
const logPath = process.env.BRIDGE_SWARM_LOG || LOG_FILE;
logStream = fs.createWriteStream(logPath, { flags: 'a' });
} catch (e) {
return null;
}
}
return logStream;
}
function log(...args) {
const stream = getLogStream();
if (stream) {
const msg = '[' + new Date().toISOString() + '] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ') + '\n';
stream.write(msg);
}
if (process.stderr) {
process.stderr.write('[' + new Date().toISOString() + '] ' + args.join(' ') + '\n');
}
}
// Load generated HRPC lazily so a missing/broken spec does not crash the host at startup
let HRPC = null;
let hrpcLoadError = null;
@@ -176,21 +204,26 @@ async function handleMessageAsync(send, msg) {
}
function closeConnection() {
if (entry._closed) return;
log('closeConnection called for', connId);
entry._closed = true;
entry.socket.removeAllListeners();
connections.delete(connId);
connToSwarm.delete(connId);
if (peerMap.get(peerKeyHex) === connId) peerMap.delete(peerKeyHex);
log('EMIT end event for conn', connId, 'swarm', swarmId);
emit('end', { connId, swarmId });
}
function onEnd() {
log('socket end for conn', connId, 'peer', peerKeyHex.slice(0, 8));
closeConnection();
}
function onError(err) {
log('socket ERROR for conn', connId, ':', err.message, err.stack);
if (!entry._closed) emit('error', { connId, swarmId, message: err.message });
closeConnection();
}
function onClose() {
function onClose(hadError) {
log('socket close for conn', connId, 'peer', peerKeyHex.slice(0, 8), 'hadError:', hadError);
closeConnection();
}
entry._listeners = { onData, onEnd, onError, onClose };
@@ -201,6 +234,7 @@ async function handleMessageAsync(send, msg) {
socket.on('end', onEnd);
socket.on('error', onError);
socket.on('close', onClose);
log('new connection', connId, 'from peer', peerKeyHex.slice(0, 8));
emit('connection', {
connId,
swarmId,
@@ -229,6 +263,22 @@ async function handleMessageAsync(send, msg) {
break;
}
case 'swarmKey': {
const { swarmId } = payload;
const swarm = swarms.get(swarmId);
if (!swarm) {
reply({ ok: false, error: 'Swarm not found' });
return;
}
const keyPair = swarm.keyPair;
if (!keyPair || !keyPair.publicKey) {
reply({ ok: false, error: 'Key pair not available yet' });
return;
}
reply({ ok: true, publicKey: b4a.toString(keyPair.publicKey, 'hex') });
break;
}
case 'leave': {
const { swarmId, topic: topicHex } = payload;
const swarm = swarms.get(swarmId);
@@ -467,15 +517,18 @@ async function handleMessageAsync(send, msg) {
case 'destroy': {
const { swarmId } = payload;
log('destroy swarm', swarmId);
const swarm = swarms.get(swarmId);
if (swarm) {
const toDestroy = [...connToSwarm.entries()].filter(([, sid]) => sid === swarmId);
for (const [cid] of toDestroy) {
const entry = connections.get(cid);
if (entry) {
log('closing conn', cid, 'peer', entry.peerKeyHex?.slice(0, 8));
entry._closed = true;
entry.socket.removeAllListeners();
peerConnections.get(swarmId)?.delete(entry.peerKeyHex);
emit('end', { connId: cid, swarmId });
try { entry.socket.destroy(); } catch (_) {}
}
connections.delete(cid);