Files

565 lines
20 KiB
JavaScript

/* FIPS management page — sovereign://fips
*
* Tabs: Status | Peers | Network | Tree
*
* - Status: FIPS + Tor status cards (sovereign://fips/status)
* - Peers: direct peers table + add-peer form (sovereign://fips/peers)
* - Network: mesh summary + known-nodes table (sovereign://fips/network)
* - Tree: spanning tree view (sovereign://fips/tree)
*
* Uses XMLHttpRequest via sovereignGet() because WebKit's custom scheme
* handler does not reliably support fetch() for sovereign:// URLs.
*/
function sovereignGet(url) {
return new Promise(function(resolve, reject) {
var x = new XMLHttpRequest();
x.open('GET', url, true);
x.onreadystatechange = function() {
if (x.readyState !== 4) return;
try { resolve(JSON.parse(x.responseText)); }
catch (e) { reject(e); }
};
x.onerror = function() { reject(new Error('XHR error')); };
x.send();
});
}
function esc(s) {
var d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
function showMsg(text, isErr) {
var el = document.getElementById('status-msg');
el.textContent = text || '';
el.className = 'status show ' + (isErr ? 'err' : 'ok');
if (!text) el.className = 'status';
}
function stateDotClass(state) {
return 'state-dot ' + (state || '');
}
/* Global: our own FIPS node npub (from show_status), used to pin our
* node in the Directory tab. */
var gOurNpub = '';
/* ── Tab switching ──────────────────────────────────────────────── */
function switchTab(tabName) {
document.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.tab === tabName);
});
document.querySelectorAll('.tab-panel').forEach(function(panel) {
panel.classList.toggle('active', panel.id === 'tab-' + tabName);
});
/* Widen the page for the directory tab (long npub list needs width). */
document.body.classList.toggle('wide', tabName === 'directory');
/* Load tab data on demand. */
if (tabName === 'network') fetchNetwork();
if (tabName === 'tree') fetchTree();
if (tabName === 'directory') fetchDirectory();
}
document.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.addEventListener('click', function() { switchTab(btn.dataset.tab); });
});
/* ── Status tab ─────────────────────────────────────────────────── */
function renderFipsStatus(data) {
var f = data.fips || {};
var state = f.state || 'disabled';
/* Store our npub for the Directory tab pinning. */
gOurNpub = f.node_npub || '';
document.getElementById('fips-state').innerHTML =
esc(state) + '<span class="' + stateDotClass(state) + '"></span>';
document.getElementById('fips-node').textContent = f.node_npub || '—';
var tun = '—';
if (f.tun_name && f.tun_name.length) {
tun = f.tun_name + (f.tun_active ? ' (active)' : ' (inactive)');
}
document.getElementById('fips-tun').textContent = tun;
document.getElementById('fips-peers').textContent =
(f.peer_count != null ? f.peer_count : '—') + ' connected';
document.getElementById('fips-tree').textContent = f.tree_state || '—';
document.getElementById('fips-ownership').textContent =
f.ownership || '—';
document.getElementById('fips-control').textContent =
f.control_socket || '—';
var errRow = document.getElementById('fips-error-row');
var errEl = document.getElementById('fips-error');
if (f.error && f.error.length) {
errEl.textContent = f.error;
errRow.style.display = '';
} else {
errRow.style.display = 'none';
}
}
function renderTorStatus(data) {
var t = data.tor || {};
var state = t.state || 'disabled';
document.getElementById('tor-state').innerHTML =
esc(state) + '<span class="' + stateDotClass(state) + '"></span>';
document.getElementById('tor-bootstrap').textContent =
(t.bootstrap_progress != null ? t.bootstrap_progress : '—') + '%';
document.getElementById('tor-socks').textContent =
t.socks_endpoint || '—';
document.getElementById('tor-ownership').textContent =
t.ownership || '—';
var errRow = document.getElementById('tor-error-row');
var errEl = document.getElementById('tor-error');
if (t.error && t.error.length) {
errEl.textContent = t.error;
errRow.style.display = '';
} else {
errRow.style.display = 'none';
}
}
function fetchStatus() {
return sovereignGet('sovereign://fips/status?_=' + Date.now())
.then(function(data) {
renderFipsStatus(data);
renderTorStatus(data);
})
.catch(function(e) {
showMsg('Failed to load status: ' + e.message, true);
});
}
/* ── Peers tab ──────────────────────────────────────────────────── */
function renderPeers(data) {
var body = document.getElementById('peers-body');
var table = document.getElementById('peers-table');
var empty = document.getElementById('peers-empty');
var peers = data.peers || [];
if (!peers.length) {
table.style.display = 'none';
empty.style.display = '';
empty.textContent = data.error
? 'No peers (' + data.error + ')'
: 'No peers connected.';
return;
}
empty.style.display = 'none';
table.style.display = '';
var html = '';
peers.forEach(function(p) {
var npub = p.npub || p.id || '';
var addr = p.transport_addr || p.address || p.addr || '';
var transport = p.transport_type || p.transport || '';
var state = p.connectivity || p.state || p.status || '';
var label = p.display_name || '';
html += '<tr>';
html += '<td class="mono">' + esc(npub) +
(label ? '<br><span class="note">' + esc(label) + '</span>' : '') +
'</td>';
html += '<td class="mono">' + esc(addr) + '</td>';
html += '<td>' + esc(transport) + '</td>';
html += '<td class="peer-state-' + esc(state) + '">' + esc(state) + '</td>';
html += '<td><button class="btn btn-del" data-npub="' + esc(npub) +
'" onclick="disconnectPeer(\'' + esc(npub).replace(/'/g, "\\'") +
'\')">✕</button></td>';
html += '</tr>';
});
body.innerHTML = html;
}
function fetchPeers() {
return sovereignGet('sovereign://fips/peers?_=' + Date.now())
.then(function(data) { renderPeers(data); })
.catch(function(e) {
document.getElementById('peers-empty').textContent =
'Failed to load peers: ' + e.message;
});
}
function connectPeer() {
var npub = document.getElementById('peer-npub').value.trim();
var address = document.getElementById('peer-address').value.trim();
var transport = document.getElementById('peer-transport').value;
if (!npub || !address) {
showMsg('npub and address are required', true);
return;
}
var url = 'sovereign://fips/connect?npub=' + encodeURIComponent(npub) +
'&address=' + encodeURIComponent(address) +
'&transport=' + encodeURIComponent(transport);
sovereignGet(url)
.then(function(d) {
if (d.error) {
showMsg('Connect failed: ' + d.error, true);
} else {
showMsg('Peer added');
document.getElementById('peer-npub').value = '';
document.getElementById('peer-address').value = '';
fetchPeers();
}
})
.catch(function(e) { showMsg('Connect failed: ' + e.message, true); });
}
function disconnectPeer(npub) {
if (!npub) return;
if (!confirm('Disconnect peer ' + npub + '?')) return;
var url = 'sovereign://fips/disconnect?npub=' + encodeURIComponent(npub);
sovereignGet(url)
.then(function(d) {
if (d.error) {
showMsg('Disconnect failed: ' + d.error, true);
} else {
showMsg('Peer disconnected');
fetchPeers();
}
})
.catch(function(e) { showMsg('Disconnect failed: ' + e.message, true); });
}
function restartService() {
if (!confirm('Restart the FIPS service? This will briefly drop all peer connections.')) return;
showMsg('Restarting FIPS service…');
sovereignGet('sovereign://fips/restart')
.then(function(d) {
if (d.error) showMsg('Restart failed: ' + d.error, true);
else showMsg('Restart initiated');
})
.catch(function(e) { showMsg('Restart failed: ' + e.message, true); });
}
/* ── Network tab ────────────────────────────────────────────────── */
function formatAge(ageMs) {
if (ageMs == null) return '—';
var s = Math.floor(ageMs / 1000);
if (s < 60) return s + 's ago';
var m = Math.floor(s / 60);
if (m < 60) return m + 'm ago';
var h = Math.floor(m / 60);
if (h < 24) return h + 'h ago';
return Math.floor(h / 24) + 'd ago';
}
function ageClass(ageMs) {
if (ageMs == null) return '';
if (ageMs < 10000) return 'last-seen-fresh';
if (ageMs < 60000) return 'last-seen-stale';
return 'last-seen-old';
}
function renderNetwork(data) {
var s = data.summary || {};
var nodes = data.nodes || [];
/* Mesh summary. */
document.getElementById('mesh-known').textContent =
(s.known_nodes != null ? s.known_nodes : '—') +
(s.max_cache_entries != null ? ' / ' + s.max_cache_entries : '');
document.getElementById('mesh-direct').textContent =
(data.peer_count != null ? data.peer_count : '—');
document.getElementById('mesh-depth').textContent =
(s.tree_depth != null ? s.tree_depth : '—');
document.getElementById('mesh-root').textContent =
s.root_npub ? esc(s.root_npub) : '—';
document.getElementById('mesh-parent').textContent =
s.parent_display_name ? esc(s.parent_display_name) :
(s.is_root ? '(this node is root)' : '—');
var errRow = document.getElementById('mesh-error-row');
var errEl = document.getElementById('mesh-error');
if (data.error) {
errEl.textContent = data.error;
errRow.style.display = '';
} else {
errRow.style.display = 'none';
}
/* Known nodes table. */
var body = document.getElementById('nodes-body');
var table = document.getElementById('nodes-table');
var empty = document.getElementById('nodes-empty');
var countLabel = document.getElementById('nodes-count-label');
countLabel.textContent = '(' + nodes.length + ' nodes)';
if (!nodes.length) {
table.style.display = 'none';
empty.style.display = '';
empty.textContent = data.error
? 'Network unavailable: ' + data.error
: 'No known nodes (FIPS may not be ready).';
return;
}
/* Sort by last_seen descending (most recent first). */
nodes.sort(function(a, b) {
var aT = a.last_seen_ms || 0;
var bT = b.last_seen_ms || 0;
return bT - aT;
});
empty.style.display = 'none';
table.style.display = '';
var html = '';
nodes.forEach(function(n) {
var npub = n.npub || '';
var name = n.display_name || '—';
var ipv6 = n.ipv6_addr || '';
var age = n.age_ms != null ? n.age_ms : null;
var direct = n.is_direct_peer;
var badge = '<span class="peer-badge' +
(direct ? '' : ' indirect') + '" title="' +
(direct ? 'direct peer' : 'discovery only') + '"></span>';
html += '<tr>';
html += '<td>' + badge + '</td>';
html += '<td class="mono">' + esc(npub) + '</td>';
html += '<td>' + esc(name) + '</td>';
html += '<td class="mono">' + esc(ipv6) + '</td>';
html += '<td class="' + ageClass(age) + '">' + formatAge(age) + '</td>';
html += '</tr>';
});
body.innerHTML = html;
}
function fetchNetwork() {
return sovereignGet('sovereign://fips/network?_=' + Date.now())
.then(function(data) { renderNetwork(data); })
.catch(function(e) {
document.getElementById('nodes-empty').textContent =
'Failed to load network: ' + e.message;
});
}
/* ── Tree tab ───────────────────────────────────────────────────── */
function renderTree(data) {
var t = data.tree || {};
var errRow = document.getElementById('tree-error-row');
var errEl = document.getElementById('tree-error');
if (data.error) {
errEl.textContent = data.error;
errRow.style.display = '';
document.getElementById('tree-peers-empty').textContent =
'Tree unavailable: ' + data.error;
document.getElementById('tree-peers-table').style.display = 'none';
/* Clear summary. */
['tree-root','tree-is-root','tree-depth','tree-parent','tree-node-addr']
.forEach(function(id) { document.getElementById(id).textContent = '—'; });
return;
}
errRow.style.display = 'none';
document.getElementById('tree-root').textContent =
t.root_npub || t.root || '—';
document.getElementById('tree-is-root').textContent =
t.is_root ? 'yes' : 'no';
document.getElementById('tree-depth').textContent =
(t.depth != null ? t.depth : '—');
document.getElementById('tree-parent').textContent =
t.parent_display_name || t.parent || '—';
document.getElementById('tree-node-addr').textContent =
t.my_node_addr || '—';
/* Tree peers (1-hop neighbors). */
var body = document.getElementById('tree-peers-body');
var table = document.getElementById('tree-peers-table');
var empty = document.getElementById('tree-peers-empty');
var peers = t.peers || [];
if (!peers.length) {
table.style.display = 'none';
empty.style.display = '';
empty.textContent = 'No tree peers.';
return;
}
empty.style.display = 'none';
table.style.display = '';
var myAddr = t.my_node_addr || '';
var parentAddr = t.parent || '';
var html = '';
peers.forEach(function(p) {
var addr = p.node_addr || '';
var name = p.display_name || '—';
var depth = p.depth != null ? p.depth : '—';
var dist = p.distance_to_us != null ? p.distance_to_us : '—';
var role = '';
if (addr === parentAddr && !t.is_root) {
role = '<span class="tree-role parent">parent</span>';
} else if (addr === t.root) {
role = '<span class="tree-role root">root</span>';
} else {
role = '<span class="tree-role child">peer</span>';
}
html += '<tr>';
html += '<td class="mono">' + esc(addr) + '</td>';
html += '<td>' + esc(name) + '</td>';
html += '<td>' + esc(depth) + '</td>';
html += '<td>' + esc(dist) + '</td>';
html += '<td>' + role + '</td>';
html += '</tr>';
});
body.innerHTML = html;
}
function fetchTree() {
return sovereignGet('sovereign://fips/tree?_=' + Date.now())
.then(function(data) { renderTree(data); })
.catch(function(e) {
document.getElementById('tree-peers-empty').textContent =
'Failed to load tree: ' + e.message;
});
}
/* ── Directory tab ───────────────────────────────────────────────── */
function renderDirectory(data) {
var body = document.getElementById('dir-body');
var table = document.getElementById('dir-table');
var empty = document.getElementById('dir-empty');
var countLabel = document.getElementById('dir-count-label');
var actions = document.getElementById('dir-actions');
var nodes = data.nodes || [];
countLabel.textContent = '(' + nodes.length + ' nodes)';
if (!nodes.length) {
table.style.display = 'none';
actions.style.display = 'none';
empty.style.display = '';
empty.textContent = data.error
? 'Directory unavailable: ' + data.error
: 'No advertised nodes found on the advert relays.';
return;
}
/* Sort: our own node first (pinned), then by created_at descending. */
nodes.sort(function(a, b) {
var aOurs = (a.npub && a.npub === gOurNpub) ? 1 : 0;
var bOurs = (b.npub && b.npub === gOurNpub) ? 1 : 0;
if (aOurs !== bOurs) return bOurs - aOurs;
var aT = a.created_at || 0;
var bT = b.created_at || 0;
return bT - aT;
});
empty.style.display = 'none';
table.style.display = '';
actions.style.display = '';
var html = '';
nodes.forEach(function(n) {
var npub = n.npub || '';
var direct = n.is_direct_peer;
var isOurs = (npub && npub === gOurNpub);
var badge = '<span class="peer-badge' +
(direct ? '' : ' indirect') + '" title="' +
(direct ? 'direct peer' : 'not connected') + '"></span>';
/* Compact endpoints: "udp 1.2.3.4:2121" (first one only, +N if more). */
var endpointsStr = '—';
if (n.endpoints && n.endpoints.length) {
var ep = n.endpoints[0];
endpointsStr = esc(ep.transport || '?') + ' ' + esc(ep.addr || '');
if (n.endpoints.length > 1) endpointsStr += ' +' + (n.endpoints.length - 1);
}
/* Connect: clickable text, not a button. */
var firstEp = (n.endpoints && n.endpoints[0]) || {};
var addr = firstEp.addr || '';
var transport = firstEp.transport || 'udp';
var connectLink = '';
if (isOurs) {
connectLink = '<span class="dir-ours-tag">this is you</span>';
} else if (addr && !direct) {
connectLink = '<a class="dir-connect-link" href="javascript:void(0)" onclick="connectFromDirectory(\'' +
esc(npub).replace(/'/g, "\\'") + '\', \'' +
esc(addr).replace(/'/g, "\\'") + '\', \'' +
esc(transport) + '\')">connect</a>';
} else if (direct) {
connectLink = '<span class="note">connected</span>';
}
var rowClass = isOurs ? ' class="dir-ours-row"' : '';
html += '<tr' + rowClass + '>';
html += '<td>' + badge + '</td>';
html += '<td class="mono">' + esc(npub) + '</td>';
html += '<td class="mono">' + endpointsStr + '</td>';
html += '<td>' + connectLink + '</td>';
html += '</tr>';
});
body.innerHTML = html;
}
function fetchDirectory() {
var empty = document.getElementById('dir-empty');
empty.style.display = '';
empty.textContent = 'Querying Nostr advert relays (this takes a few seconds)…';
document.getElementById('dir-table').style.display = 'none';
document.getElementById('dir-actions').style.display = 'none';
return sovereignGet('sovereign://fips/directory?_=' + Date.now())
.then(function(data) { renderDirectory(data); })
.catch(function(e) {
empty.textContent = 'Failed to load directory: ' + e.message;
});
}
function connectFromDirectory(npub, address, transport) {
/* Switch to the Peers tab, pre-fill the add-peer form, and connect. */
switchTab('peers');
document.getElementById('peer-npub').value = npub;
document.getElementById('peer-address').value = address;
var sel = document.getElementById('peer-transport');
for (var i = 0; i < sel.options.length; i++) {
if (sel.options[i].value === transport) {
sel.selectedIndex = i;
break;
}
}
connectPeer();
}
/* ── Refresh + auto-refresh ─────────────────────────────────────── */
function refresh() {
showMsg('');
var activeTab = document.querySelector('.tab-btn.active');
var tabName = activeTab ? activeTab.dataset.tab : 'status';
var promises = [fetchStatus(), fetchPeers()];
if (tabName === 'network') promises.push(fetchNetwork());
if (tabName === 'tree') promises.push(fetchTree());
Promise.all(promises).then(function() {});
}
document.getElementById('refresh-btn').addEventListener('click', refresh);
document.getElementById('connect-btn').addEventListener('click', connectPeer);
document.getElementById('restart-btn').addEventListener('click', restartService);
var dirRefreshBtn = document.getElementById('dir-refresh-btn');
if (dirRefreshBtn) dirRefreshBtn.addEventListener('click', fetchDirectory);
/* Initial load. */
refresh();
/* Auto-refresh status + peers every 5s. Network/tree refresh on
* tab activation and every 10s when active. */
setInterval(function() {
fetchStatus();
fetchPeers();
}, 5000);
setInterval(function() {
var activeTab = document.querySelector('.tab-btn.active');
if (!activeTab) return;
var tabName = activeTab.dataset.tab;
if (tabName === 'network') fetchNetwork();
if (tabName === 'tree') fetchTree();
}, 10000);