@relay-federation/bridge 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/index.html +326 -49
- package/lib/status-server.js +2 -0
- package/package.json +1 -1
package/dashboard/index.html
CHANGED
|
@@ -76,6 +76,24 @@
|
|
|
76
76
|
white-space: nowrap;
|
|
77
77
|
}
|
|
78
78
|
.header h1 span { color: var(--accent-blue); }
|
|
79
|
+
.protocol-badge {
|
|
80
|
+
font-family: var(--mono);
|
|
81
|
+
font-size: 12px;
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
color: var(--accent-blue);
|
|
84
|
+
padding: 3px 10px;
|
|
85
|
+
border-radius: 20px;
|
|
86
|
+
border: 1px solid rgba(88,166,255,0.3);
|
|
87
|
+
background: rgba(88,166,255,0.08);
|
|
88
|
+
text-shadow: 0 0 8px rgba(88,166,255,0.5);
|
|
89
|
+
box-shadow: 0 0 12px rgba(88,166,255,0.15), inset 0 0 8px rgba(88,166,255,0.05);
|
|
90
|
+
letter-spacing: 1px;
|
|
91
|
+
animation: protocolPulse 3s ease-in-out infinite;
|
|
92
|
+
}
|
|
93
|
+
@keyframes protocolPulse {
|
|
94
|
+
0%, 100% { box-shadow: 0 0 12px rgba(88,166,255,0.15), inset 0 0 8px rgba(88,166,255,0.05); }
|
|
95
|
+
50% { box-shadow: 0 0 20px rgba(88,166,255,0.3), inset 0 0 12px rgba(88,166,255,0.1); }
|
|
96
|
+
}
|
|
79
97
|
.mesh-badge {
|
|
80
98
|
display: flex;
|
|
81
99
|
align-items: center;
|
|
@@ -229,7 +247,11 @@
|
|
|
229
247
|
}
|
|
230
248
|
.detail-card {
|
|
231
249
|
padding: 16px 20px;
|
|
250
|
+
cursor: pointer;
|
|
251
|
+
transition: border-color 0.2s;
|
|
252
|
+
border: 1px solid transparent;
|
|
232
253
|
}
|
|
254
|
+
.detail-card:hover { border-color: rgba(88,166,255,0.2); }
|
|
233
255
|
.detail-card h3 {
|
|
234
256
|
color: var(--text-muted);
|
|
235
257
|
font-size: 10px;
|
|
@@ -238,6 +260,50 @@
|
|
|
238
260
|
margin-bottom: 10px;
|
|
239
261
|
font-weight: 500;
|
|
240
262
|
}
|
|
263
|
+
.card-overlay {
|
|
264
|
+
position: fixed;
|
|
265
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
266
|
+
z-index: 9999;
|
|
267
|
+
background: rgba(0,0,0,0.85);
|
|
268
|
+
display: flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
justify-content: center;
|
|
271
|
+
}
|
|
272
|
+
.card-overlay .card-expanded {
|
|
273
|
+
position: relative;
|
|
274
|
+
width: 90vw;
|
|
275
|
+
max-width: 700px;
|
|
276
|
+
max-height: 85vh;
|
|
277
|
+
overflow-y: auto;
|
|
278
|
+
border-radius: var(--radius);
|
|
279
|
+
background: radial-gradient(ellipse at center, rgba(20,30,50,0.95) 0%, rgba(8,10,14,0.98) 100%);
|
|
280
|
+
border: 1px solid var(--border-light);
|
|
281
|
+
padding: 24px 28px;
|
|
282
|
+
}
|
|
283
|
+
.card-overlay .card-expanded h2 {
|
|
284
|
+
color: var(--text-primary);
|
|
285
|
+
font-size: 14px;
|
|
286
|
+
text-transform: uppercase;
|
|
287
|
+
letter-spacing: 1.2px;
|
|
288
|
+
margin-bottom: 16px;
|
|
289
|
+
font-weight: 500;
|
|
290
|
+
}
|
|
291
|
+
.card-overlay .card-close {
|
|
292
|
+
position: absolute;
|
|
293
|
+
top: 12px; right: 16px;
|
|
294
|
+
z-index: 10;
|
|
295
|
+
background: rgba(0,0,0,0.5);
|
|
296
|
+
border: 1px solid var(--border);
|
|
297
|
+
color: var(--text-muted);
|
|
298
|
+
font-size: 18px;
|
|
299
|
+
width: 32px; height: 32px;
|
|
300
|
+
border-radius: 50%;
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
justify-content: center;
|
|
305
|
+
}
|
|
306
|
+
.card-overlay .card-close:hover { color: var(--text-primary); background: rgba(255,255,255,0.1); }
|
|
241
307
|
.panel-row {
|
|
242
308
|
display: flex;
|
|
243
309
|
justify-content: space-between;
|
|
@@ -872,6 +938,7 @@
|
|
|
872
938
|
<div class="header">
|
|
873
939
|
<div class="header-left">
|
|
874
940
|
<h1><span>Federation</span> Mesh</h1>
|
|
941
|
+
<span class="protocol-badge">70016</span>
|
|
875
942
|
<div class="mesh-badge">
|
|
876
943
|
<span class="status-dot green pulse" id="meshDot"></span>
|
|
877
944
|
<span id="meshStatus">loading</span>
|
|
@@ -1009,6 +1076,20 @@ let BRIDGES = SEED_URLS.map(url => ({ name: null, url }));
|
|
|
1009
1076
|
const POLL_INTERVAL = 5000;
|
|
1010
1077
|
let discoveryDone = false;
|
|
1011
1078
|
|
|
1079
|
+
const BOND_EXPLAINER = '<h2>Surety Bond (BND)</h2>' +
|
|
1080
|
+
'<div style="color:var(--text-muted);font-size:13px;line-height:1.6;max-width:480px">' +
|
|
1081
|
+
'<p style="margin-bottom:12px"><strong style="color:var(--text-primary)">This is NOT proof-of-stake.</strong> There is no staking, no delegation, and no block rewards.</p>' +
|
|
1082
|
+
'<p style="margin-bottom:12px">A <strong>surety bond</strong> is economic collateral. Bridge operators lock BSV to their own address as a signal of commitment. The bond UTXO is monitored on-chain — if a bridge spends its bond, the network flags it and its reputation score drops.</p>' +
|
|
1083
|
+
'<p style="margin-bottom:12px">Think of it like a security deposit: you get it back when you leave, but spending it while active tells the network you may not be serious.</p>' +
|
|
1084
|
+
'<p style="margin-bottom:12px"><strong style="color:var(--text-primary)">How BND score works:</strong></p>' +
|
|
1085
|
+
'<ul style="margin:0 0 12px 16px;padding:0">' +
|
|
1086
|
+
'<li style="margin-bottom:6px">Bond amount — more BSV locked = higher score</li>' +
|
|
1087
|
+
'<li style="margin-bottom:6px">Bond age — longer held unspent = more trust</li>' +
|
|
1088
|
+
'<li>Minimum bond: 0.01 BSV (1,000,000 satoshis)</li>' +
|
|
1089
|
+
'</ul>' +
|
|
1090
|
+
'<p style="color:var(--text-dim);font-size:11px">BSV disabled OP_CHECKLOCKTIMEVERIFY at Genesis (Feb 2020), so script-level timelocks are not possible. Enforcement is done by monitoring the UTXO.</p>' +
|
|
1091
|
+
'</div>';
|
|
1092
|
+
|
|
1012
1093
|
// ── State ──────────────────────────────────────────
|
|
1013
1094
|
let bridgeData = new Map();
|
|
1014
1095
|
let selectedBridge = null;
|
|
@@ -1020,6 +1101,189 @@ let savedExplorerInput = '';
|
|
|
1020
1101
|
let savedExplorerResult = '';
|
|
1021
1102
|
let appsData = null;
|
|
1022
1103
|
let latestPrice = null;
|
|
1104
|
+
let mempoolHistory = [];
|
|
1105
|
+
let peerCountHistory = [];
|
|
1106
|
+
|
|
1107
|
+
function openCardOverlay(type) {
|
|
1108
|
+
const b = bridgeData.get(selectedBridge);
|
|
1109
|
+
if (!b) return;
|
|
1110
|
+
const op = isOperator(b._url);
|
|
1111
|
+
let html = renderCardExpanded(type, b, op);
|
|
1112
|
+
if (!html) return;
|
|
1113
|
+
const overlay = document.createElement('div');
|
|
1114
|
+
overlay.className = 'card-overlay';
|
|
1115
|
+
overlay.id = 'cardOverlay';
|
|
1116
|
+
overlay.innerHTML = '<div class="card-expanded"><button class="card-close" onclick="closeCardOverlay()">×</button>' + html + '</div>';
|
|
1117
|
+
document.body.appendChild(overlay);
|
|
1118
|
+
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeCardOverlay(); });
|
|
1119
|
+
// Async: load wallet tx history
|
|
1120
|
+
if (type === 'wallet' && b.bridge.address) {
|
|
1121
|
+
fetch('/address/' + b.bridge.address + '/history', { signal: AbortSignal.timeout(10000) })
|
|
1122
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
1123
|
+
.then(function(data) {
|
|
1124
|
+
var el = document.getElementById('walletTxList');
|
|
1125
|
+
if (!el || !data || !data.history) { if (el) el.textContent = 'No transactions found'; return; }
|
|
1126
|
+
var txs = data.history.slice(0, 10);
|
|
1127
|
+
if (txs.length === 0) { el.textContent = 'No transactions yet'; return; }
|
|
1128
|
+
var h = '';
|
|
1129
|
+
for (var i = 0; i < txs.length; i++) {
|
|
1130
|
+
var tx = txs[i];
|
|
1131
|
+
var txid = tx.tx_hash || tx.txid || tx;
|
|
1132
|
+
var short = typeof txid === 'string' ? txid.slice(0, 10) + '...' + txid.slice(-6) : '?';
|
|
1133
|
+
var height = tx.height || '';
|
|
1134
|
+
h += '<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid var(--border)">';
|
|
1135
|
+
h += '<a href="https://whatsonchain.com/tx/' + txid + '" target="_blank" style="color:var(--accent-blue);text-decoration:none;font-family:var(--mono)">' + short + '</a>';
|
|
1136
|
+
if (height) h += '<span style="color:var(--text-dim);font-size:10px">block ' + height + '</span>';
|
|
1137
|
+
h += '</div>';
|
|
1138
|
+
}
|
|
1139
|
+
el.innerHTML = h;
|
|
1140
|
+
})
|
|
1141
|
+
.catch(function() {
|
|
1142
|
+
var el = document.getElementById('walletTxList');
|
|
1143
|
+
if (el) el.textContent = 'Failed to load transactions';
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function closeCardOverlay() {
|
|
1148
|
+
const el = document.getElementById('cardOverlay');
|
|
1149
|
+
if (el) el.remove();
|
|
1150
|
+
}
|
|
1151
|
+
function renderCardExpanded(type, b, op) {
|
|
1152
|
+
let html = '';
|
|
1153
|
+
switch (type) {
|
|
1154
|
+
case 'bridge':
|
|
1155
|
+
html += '<h2>Bridge</h2>';
|
|
1156
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px">Identity</div>';
|
|
1157
|
+
html += '<div class="panel-row"><span class="label">Status</span><span class="value ' + (b._error ? 'red' : 'green') + '">' + (b._error ? 'offline' : 'online') + '</span></div>';
|
|
1158
|
+
html += '<div class="panel-row"><span class="label">Registration</span><span class="value ' + (b.bridge.endpoint ? 'green' : 'yellow') + '">' + (b.bridge.endpoint ? 'Registered' : 'Unregistered') + '</span></div>';
|
|
1159
|
+
html += '<div class="panel-row"><span class="label">Pubkey</span><span class="value" style="font-size:10px;word-break:break-all;cursor:pointer" onclick="event.stopPropagation();copyText(\'' + (b.bridge.pubkeyHex || '') + '\', this)">' + (b.bridge.pubkeyHex || '-') + '</span></div>';
|
|
1160
|
+
html += '<div class="panel-row"><span class="label">Endpoint</span><span class="value" style="cursor:pointer" onclick="event.stopPropagation();copyText(\'' + (b.bridge.endpoint || '') + '\', this)">' + (b.bridge.endpoint || '-') + '</span></div>';
|
|
1161
|
+
if (b.bridge.address) {
|
|
1162
|
+
html += '<div class="panel-row"><span class="label">Address</span><span class="value" style="font-size:10px;word-break:break-all;cursor:pointer" onclick="event.stopPropagation();copyText(\'' + b.bridge.address + '\', this)">' + b.bridge.address + '</span></div>';
|
|
1163
|
+
}
|
|
1164
|
+
html += '<div class="panel-row"><span class="label">Mesh ID</span><span class="value">' + (b.bridge.meshId || '-') + '</span></div>';
|
|
1165
|
+
// Domains as clickable links
|
|
1166
|
+
if (b.bridge.domains && b.bridge.domains.length > 0) {
|
|
1167
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin:16px 0 12px;text-transform:uppercase;letter-spacing:1px">Domains</div>';
|
|
1168
|
+
for (var di = 0; di < b.bridge.domains.length; di++) {
|
|
1169
|
+
var d = b.bridge.domains[di];
|
|
1170
|
+
html += '<div class="panel-row"><span class="value"><a href="https://' + d + '" target="_blank" style="color:var(--accent-blue);text-decoration:none">' + d + '</a></span></div>';
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin:16px 0 12px;text-transform:uppercase;letter-spacing:1px">Uptime</div>';
|
|
1174
|
+
html += '<div class="panel-row"><span class="label">Current</span><span class="value">' + fmtUptime(b.bridge.uptimeSeconds) + '</span></div>';
|
|
1175
|
+
var maxUptime = Math.max(...[...bridgeData.values()].map(bb => bb.bridge.uptimeSeconds || 0), 1);
|
|
1176
|
+
var uptimePct = Math.min(100, (b.bridge.uptimeSeconds / maxUptime) * 100);
|
|
1177
|
+
html += '<div style="margin-top:8px"><div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:4px"><span style="color:var(--text-dim)">vs mesh leader</span><span style="color:var(--text-muted)">' + uptimePct.toFixed(0) + '%</span></div>';
|
|
1178
|
+
html += '<div style="height:6px;background:var(--border);border-radius:3px;overflow:hidden"><div style="height:100%;width:' + uptimePct + '%;background:var(--accent-green);border-radius:3px;transition:width 0.5s"></div></div></div>';
|
|
1179
|
+
// Peer count sparkline
|
|
1180
|
+
if (peerCountHistory.length > 1) {
|
|
1181
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin:16px 0 12px;text-transform:uppercase;letter-spacing:1px">Peer Connections (last ' + peerCountHistory.length + ' polls)</div>';
|
|
1182
|
+
html += renderSparkline(peerCountHistory, 'rgba(63,185,80,0.7)');
|
|
1183
|
+
}
|
|
1184
|
+
break;
|
|
1185
|
+
case 'network':
|
|
1186
|
+
html += '<h2>Network</h2>';
|
|
1187
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px">Chain</div>';
|
|
1188
|
+
html += '<div class="panel-row"><span class="label">Best Height</span><span class="value green">' + fmtHeight(b.headers.bestHeight) + '</span></div>';
|
|
1189
|
+
html += '<div class="panel-row"><span class="label">Best Hash</span><span class="value" style="font-size:10px;word-break:break-all">' + (b.headers.bestHash || '-') + '</span></div>';
|
|
1190
|
+
html += '<div class="panel-row"><span class="label">Local Headers</span><span class="value">' + b.headers.count.toLocaleString() + '</span></div>';
|
|
1191
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin:16px 0 12px;text-transform:uppercase;letter-spacing:1px">Connectivity</div>';
|
|
1192
|
+
html += '<div class="panel-row"><span class="label">BSV P2P</span><span class="value ' + (b.bsvNode && b.bsvNode.connected ? 'green' : 'red') + '">' + (b.bsvNode && b.bsvNode.connected ? 'Connected' : 'Disconnected') + ' — ' + (b.bsvNode ? b.bsvNode.peers : 0) + ' peers</span></div>';
|
|
1193
|
+
if (b.bsvNode && b.bsvNode.height) {
|
|
1194
|
+
html += '<div class="panel-row"><span class="label">Node Height</span><span class="value">' + b.bsvNode.height.toLocaleString() + '</span></div>';
|
|
1195
|
+
}
|
|
1196
|
+
html += '<div class="panel-row"><span class="label">Mesh Bridges</span><span class="value">' + b.peers.connected + ' connected</span></div>';
|
|
1197
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin:16px 0 12px;text-transform:uppercase;letter-spacing:1px">Transactions</div>';
|
|
1198
|
+
html += '<div class="panel-row"><span class="label">Mempool</span><span class="value">' + b.txs.mempool + ' txs</span></div>';
|
|
1199
|
+
html += '<div class="panel-row"><span class="label">Seen</span><span class="value">' + b.txs.seen + ' txs</span></div>';
|
|
1200
|
+
// Mempool sparkline (last 20 polls)
|
|
1201
|
+
if (mempoolHistory.length > 1) {
|
|
1202
|
+
html += '<div style="margin-top:12px"><div style="font-size:11px;color:var(--text-dim);margin-bottom:6px">Mempool History (last ' + mempoolHistory.length + ' polls)</div>';
|
|
1203
|
+
html += renderSparkline(mempoolHistory, 'rgba(88,166,255,0.7)');
|
|
1204
|
+
html += '</div>';
|
|
1205
|
+
}
|
|
1206
|
+
break;
|
|
1207
|
+
case 'wallet':
|
|
1208
|
+
if (!op || !b.wallet) return '';
|
|
1209
|
+
// Big balance hero
|
|
1210
|
+
var balSats = b.wallet.balanceSats || 0;
|
|
1211
|
+
var balUsd = (latestPrice && latestPrice.usd) ? (balSats / 1e8 * latestPrice.usd).toFixed(2) : null;
|
|
1212
|
+
html += '<div style="text-align:center;padding:20px 0 16px">';
|
|
1213
|
+
html += '<div style="font-size:32px;font-weight:700;color:var(--accent-green);letter-spacing:-1px">' + fmtSats(balSats) + ' <span style="font-size:14px;font-weight:400;color:var(--text-muted)">sats</span></div>';
|
|
1214
|
+
if (balUsd) html += '<div style="font-size:18px;color:var(--accent-blue);margin-top:4px">$' + balUsd + ' <span style="font-size:11px;color:var(--text-dim)">USD</span></div>';
|
|
1215
|
+
html += '</div>';
|
|
1216
|
+
// Send button
|
|
1217
|
+
html += '<div style="text-align:center;margin-bottom:20px"><button class="btn primary" style="padding:8px 32px;font-size:13px" onclick="event.stopPropagation();closeCardOverlay();showSendModal(\'' + b._url + '\')">Send BSV</button></div>';
|
|
1218
|
+
// Address + QR
|
|
1219
|
+
html += '<div style="border-top:1px solid var(--border);padding-top:16px">';
|
|
1220
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px">Address</div>';
|
|
1221
|
+
if (b.bridge.address) {
|
|
1222
|
+
html += '<div style="text-align:center;font-size:11px;color:var(--text-muted);word-break:break-all;cursor:pointer;margin-bottom:12px" onclick="event.stopPropagation();copyText(\'' + b.bridge.address + '\', this)">' + b.bridge.address + '</div>';
|
|
1223
|
+
html += '<div style="text-align:center;padding-bottom:16px"><img src="' + generateQR(b.bridge.address) + '" alt="QR" style="border-radius:8px;border:2px solid var(--border)" width="180" height="180"></div>';
|
|
1224
|
+
}
|
|
1225
|
+
html += '</div>';
|
|
1226
|
+
// UTXOs
|
|
1227
|
+
html += '<div style="border-top:1px solid var(--border);padding-top:12px">';
|
|
1228
|
+
html += '<div class="panel-row"><span class="label">UTXOs</span><span class="value">' + (b.wallet.utxoCount || 0) + '</span></div>';
|
|
1229
|
+
html += '</div>';
|
|
1230
|
+
// Recent transactions — loaded async
|
|
1231
|
+
if (b.bridge.address) {
|
|
1232
|
+
html += '<div style="border-top:1px solid var(--border);padding-top:12px;margin-top:12px">';
|
|
1233
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px">Recent Transactions</div>';
|
|
1234
|
+
html += '<div id="walletTxList" style="font-size:11px;color:var(--text-dim)">Loading...</div>';
|
|
1235
|
+
html += '</div>';
|
|
1236
|
+
}
|
|
1237
|
+
break;
|
|
1238
|
+
case 'bsvnode':
|
|
1239
|
+
if (!b.bsvNode) return '';
|
|
1240
|
+
html += '<h2>BSV Node</h2>';
|
|
1241
|
+
html += '<div class="panel-row"><span class="label">Status</span><span class="value ' + (b.bsvNode.connected ? 'green' : 'red') + '">' + (b.bsvNode.connected ? 'Connected' : 'Disconnected') + '</span></div>';
|
|
1242
|
+
html += '<div class="panel-row"><span class="label">Peers</span><span class="value">' + (b.bsvNode.peers || 0) + '</span></div>';
|
|
1243
|
+
html += '<div class="panel-row"><span class="label">Node Height</span><span class="value">' + (b.bsvNode.height ? b.bsvNode.height.toLocaleString() : '-') + '</span></div>';
|
|
1244
|
+
html += '<div class="panel-row"><span class="label">Bridge Headers</span><span class="value">' + b.headers.count.toLocaleString() + '</span></div>';
|
|
1245
|
+
if (b.bsvNode.height && b.headers.count) {
|
|
1246
|
+
const syncPct = Math.min(100, (b.headers.count / b.bsvNode.height * 100)).toFixed(1);
|
|
1247
|
+
html += '<div class="panel-row"><span class="label">Sync</span><span class="value">' + syncPct + '%</span></div>';
|
|
1248
|
+
}
|
|
1249
|
+
break;
|
|
1250
|
+
case 'peers':
|
|
1251
|
+
// Only show connected peers
|
|
1252
|
+
var onlinePeers = b.peers.list.filter(function(p) { return p.connected; });
|
|
1253
|
+
html += '<h2>Peers (' + onlinePeers.length + ')</h2>';
|
|
1254
|
+
// Sort by score descending
|
|
1255
|
+
var sortedPeers = onlinePeers.slice().sort(function(a, c) { return (c.score || 0) - (a.score || 0); });
|
|
1256
|
+
if (sortedPeers.length === 0) {
|
|
1257
|
+
html += '<div style="color:var(--text-dim);font-size:12px;padding:12px 0">No peers connected</div>';
|
|
1258
|
+
}
|
|
1259
|
+
for (var pi = 0; pi < sortedPeers.length; pi++) {
|
|
1260
|
+
var p = sortedPeers[pi];
|
|
1261
|
+
var scoreVal = p.score !== undefined ? p.score.toFixed(2) : '?';
|
|
1262
|
+
var scoreClr = p.score >= 0.7 ? 'var(--accent-green)' : p.score >= 0.4 ? 'var(--accent-yellow)' : 'var(--accent-red)';
|
|
1263
|
+
html += '<div style="padding:10px 0;border-bottom:1px solid var(--border)">';
|
|
1264
|
+
html += '<div style="display:flex;align-items:center;justify-content:space-between">';
|
|
1265
|
+
html += '<div><span class="peer-dot green"></span><span class="peer-name" style="cursor:pointer" onclick="event.stopPropagation();copyText(\'' + (p.pubkeyHex || '') + '\', this)">' + truncPubkey(p.pubkeyHex) + '</span></div>';
|
|
1266
|
+
html += '<span style="font-family:var(--mono);font-size:13px;font-weight:600;color:' + scoreClr + '">' + scoreVal + '</span>';
|
|
1267
|
+
html += '</div>';
|
|
1268
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin-top:2px">' + (p.endpoint || 'no endpoint') + (p.health ? ' · ' + p.health : '') + '</div>';
|
|
1269
|
+
if (p.scoreBreakdown) {
|
|
1270
|
+
html += '<div style="margin-top:6px">' + renderScoreBars(p.scoreBreakdown) + '</div>';
|
|
1271
|
+
}
|
|
1272
|
+
html += '</div>';
|
|
1273
|
+
}
|
|
1274
|
+
// Surety bond explainer
|
|
1275
|
+
html += '<div style="margin-top:16px;padding:12px;border:1px solid var(--border);border-radius:8px;background:rgba(255,255,255,0.03)">';
|
|
1276
|
+
html += '<h3 style="margin-bottom:8px;font-size:11px;letter-spacing:1px">SURETY BOND (BND)</h3>';
|
|
1277
|
+
html += '<div style="color:var(--text-muted);font-size:12px;line-height:1.5">';
|
|
1278
|
+
html += '<p style="margin-bottom:8px"><strong style="color:var(--text-primary)">This is not proof-of-stake.</strong> No staking, no delegation, no block rewards.</p>';
|
|
1279
|
+
html += '<p style="margin-bottom:8px">A surety bond is economic collateral. Operators lock BSV to their own address as a signal of commitment. The UTXO is monitored on-chain — spending your bond while active flags your bridge and drops your reputation.</p>';
|
|
1280
|
+
html += '<p style="margin-bottom:8px">More BSV bonded + longer held unspent = higher BND score. Minimum: 0.01 BSV.</p>';
|
|
1281
|
+
html += '</div></div>';
|
|
1282
|
+
break;
|
|
1283
|
+
default: return '';
|
|
1284
|
+
}
|
|
1285
|
+
return html;
|
|
1286
|
+
}
|
|
1023
1287
|
|
|
1024
1288
|
try { const saved = localStorage.getItem('relay_operator_tokens'); if (saved) operatorTokens = JSON.parse(saved); } catch {}
|
|
1025
1289
|
|
|
@@ -1032,6 +1296,20 @@ function fmtUptime(s) {
|
|
|
1032
1296
|
return m + 'm';
|
|
1033
1297
|
}
|
|
1034
1298
|
function truncPubkey(pk) { return pk ? pk.slice(0, 8) + ' ... ' + pk.slice(-6) : '(none)'; }
|
|
1299
|
+
function copyText(text, el) { navigator.clipboard.writeText(text).then(() => { var orig = el.textContent; el.textContent = 'Copied!'; el.style.color = 'var(--accent-green)'; setTimeout(() => { el.textContent = orig; el.style.color = ''; }, 1200); }); }
|
|
1300
|
+
function renderSparkline(data, color) {
|
|
1301
|
+
if (data.length < 2) return '';
|
|
1302
|
+
var max = Math.max(...data, 1), w = 300, h = 50, pts = [];
|
|
1303
|
+
for (var i = 0; i < data.length; i++) {
|
|
1304
|
+
var x = (i / (data.length - 1)) * w;
|
|
1305
|
+
var y = h - (data[i] / max) * (h - 4) - 2;
|
|
1306
|
+
pts.push(x.toFixed(1) + ',' + y.toFixed(1));
|
|
1307
|
+
}
|
|
1308
|
+
var svg = '<svg width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '" style="width:100%;height:50px">';
|
|
1309
|
+
svg += '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + color + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
|
1310
|
+
svg += '<polygon points="0,' + h + ' ' + pts.join(' ') + ' ' + w + ',' + h + '" fill="' + color.replace('0.7', '0.1') + '"/>';
|
|
1311
|
+
return svg + '</svg>';
|
|
1312
|
+
}
|
|
1035
1313
|
function scoreColor(v) { return v >= 0.7 ? 'green' : v >= 0.4 ? 'yellow' : 'red'; }
|
|
1036
1314
|
function fmtSats(n) { return n === null || n === undefined ? '-' : n.toLocaleString(); }
|
|
1037
1315
|
function fmtHeight(n) { return n > 0 ? n.toLocaleString() : '-'; }
|
|
@@ -1123,9 +1401,13 @@ async function pollAll() {
|
|
|
1123
1401
|
if (!selectedBridge && bridgeData.size > 0) {
|
|
1124
1402
|
selectedBridge = bridgeData.keys().next().value;
|
|
1125
1403
|
}
|
|
1404
|
+
// Track history for sparklines (last 20 polls)
|
|
1405
|
+
const sel = bridgeData.get(selectedBridge);
|
|
1406
|
+
if (sel && sel.txs) { mempoolHistory.push(sel.txs.mempool); if (mempoolHistory.length > 20) mempoolHistory.shift(); }
|
|
1407
|
+
if (sel && sel.peers) { peerCountHistory.push(sel.peers.connected); if (peerCountHistory.length > 20) peerCountHistory.shift(); }
|
|
1126
1408
|
// Fetch price from self (same-origin, always works)
|
|
1127
1409
|
fetch('/price', { signal: AbortSignal.timeout(5000) })
|
|
1128
|
-
.then(r => r.ok ? r.json() : null).then(d => { if (d) latestPrice = d; }).catch(() => {});
|
|
1410
|
+
.then(r => r.ok ? r.json() : null).then(d => { if (d) { latestPrice = d; const el = document.querySelector('.stats-hero .stat-card:last-child .stat-value'); if (el) el.textContent = '$' + d.usd.toFixed(2); } }).catch(() => {});
|
|
1129
1411
|
renderHeader();
|
|
1130
1412
|
renderBridgeRail();
|
|
1131
1413
|
renderActiveTab(true);
|
|
@@ -1138,14 +1420,13 @@ function renderHeader() {
|
|
|
1138
1420
|
const totalPeers = online.reduce((s, b) => s + b.peers.connected, 0);
|
|
1139
1421
|
const bestH = Math.max(...all.map(b => b.headers.bestHeight));
|
|
1140
1422
|
|
|
1141
|
-
document.getElementById('hBridgeCount').textContent = online.length
|
|
1423
|
+
document.getElementById('hBridgeCount').textContent = online.length;
|
|
1142
1424
|
document.getElementById('hTotalPeers').textContent = totalPeers;
|
|
1143
1425
|
document.getElementById('hBestHeight').textContent = bestH > 0 ? bestH.toLocaleString() : '-';
|
|
1144
1426
|
|
|
1145
1427
|
const dot = document.getElementById('meshDot');
|
|
1146
1428
|
const status = document.getElementById('meshStatus');
|
|
1147
|
-
if (online.length
|
|
1148
|
-
else if (online.length > 0) { dot.className = 'status-dot yellow pulse'; status.textContent = 'degraded'; }
|
|
1429
|
+
if (online.length > 0) { dot.className = 'status-dot green pulse'; status.textContent = 'healthy'; }
|
|
1149
1430
|
else { dot.className = 'status-dot red pulse'; status.textContent = 'offline'; }
|
|
1150
1431
|
}
|
|
1151
1432
|
|
|
@@ -1154,15 +1435,17 @@ function renderBridgeRail() {
|
|
|
1154
1435
|
const el = document.getElementById('bridgeRail');
|
|
1155
1436
|
let html = '<div class="bridge-rail-header">Bridges</div>';
|
|
1156
1437
|
for (const [name, b] of bridgeData) {
|
|
1157
|
-
|
|
1438
|
+
if (b._error) continue; // Skip offline bridges
|
|
1439
|
+
const dotColor = b.peers.connected > 0 ? 'green' : 'yellow';
|
|
1158
1440
|
const sel = selectedBridge === name ? ' selected' : '';
|
|
1159
1441
|
html += '<div class="rail-item' + sel + '" onclick="selectBridge(\'' + name + '\')">';
|
|
1160
1442
|
html += '<span class="status-dot ' + dotColor + '"></span>';
|
|
1161
1443
|
html += '<div style="display:flex;flex-direction:column;flex:1;min-width:0"><span class="rail-name">' + name + '</span>';
|
|
1162
1444
|
const ip = (b._url || '').replace(/^https?:\/\//, '').replace(/:\d+$/, '');
|
|
1163
1445
|
html += '<span style="font-size:10px;color:var(--text-muted);opacity:0.6">' + ip + '</span></div>';
|
|
1164
|
-
|
|
1165
|
-
|
|
1446
|
+
html += '<div style="text-align:right"><span class="rail-height">' + fmtHeight(b.headers.bestHeight) + '</span>';
|
|
1447
|
+
if (b.bridge && b.bridge.version) html += '<div style="font-size:9px;color:var(--text-dim)">v' + b.bridge.version + '</div>';
|
|
1448
|
+
html += '</div>';
|
|
1166
1449
|
html += '</div>';
|
|
1167
1450
|
}
|
|
1168
1451
|
el.innerHTML = html;
|
|
@@ -1245,7 +1528,7 @@ function renderOverviewTab() {
|
|
|
1245
1528
|
const totalMempool = online.reduce((s, b) => s + (b.txs ? b.txs.mempool : 0), 0);
|
|
1246
1529
|
|
|
1247
1530
|
let html = '<div class="stats-hero">';
|
|
1248
|
-
html += '<div class="stat-card glass"><div class="stat-value">' + online.length + '
|
|
1531
|
+
html += '<div class="stat-card glass"><div class="stat-value">' + online.length + '</div><div class="stat-label">Bridges Online</div></div>';
|
|
1249
1532
|
html += '<div class="stat-card glass"><div class="stat-value green">' + fmtHeight(bestH) + '</div><div class="stat-label">Best Height</div></div>';
|
|
1250
1533
|
html += '<div class="stat-card glass"><div class="stat-value">' + totalPeers + '</div><div class="stat-label">Mesh Peers</div></div>';
|
|
1251
1534
|
html += '<div class="stat-card glass"><div class="stat-value">' + totalMempool + '</div><div class="stat-label">Mempool Txs</div></div>';
|
|
@@ -1269,6 +1552,13 @@ function renderOverviewTab() {
|
|
|
1269
1552
|
else if (!op && isOperator(b._url)) html += '<button class="btn sm" onclick="showAuthModal(\'' + b._url + '\')">Re-auth</button>';
|
|
1270
1553
|
html += '</div>';
|
|
1271
1554
|
|
|
1555
|
+
// Version + timestamp
|
|
1556
|
+
var myVer = b.bridge && b.bridge.version ? b.bridge.version : '?';
|
|
1557
|
+
var allVersions = [...bridgeData.values()].filter(x => !x._error && x.bridge && x.bridge.version).map(x => x.bridge.version);
|
|
1558
|
+
var newestVer = allVersions.length > 0 ? allVersions.sort().pop() : myVer;
|
|
1559
|
+
var verColor = myVer === newestVer ? 'var(--text-muted)' : 'var(--accent-yellow)';
|
|
1560
|
+
var verWarn = myVer !== newestVer ? ' (latest: v' + newestVer + ')' : '';
|
|
1561
|
+
html += '<div style="font-size:11px;margin-bottom:12px"><span style="color:' + verColor + ';font-weight:600">v' + myVer + verWarn + '</span> <span style="color:var(--text-dim)">· ' + new Date().toLocaleString() + '</span></div>';
|
|
1272
1562
|
if (b._error) html += '<div class="error-banner">' + b._error + '</div>';
|
|
1273
1563
|
|
|
1274
1564
|
// 3D Mesh Map — compact inline, click to expand
|
|
@@ -1279,10 +1569,22 @@ function renderOverviewTab() {
|
|
|
1279
1569
|
html += '</div>';
|
|
1280
1570
|
}
|
|
1281
1571
|
|
|
1572
|
+
// Operator toolbar (spans full width, under topology)
|
|
1573
|
+
if (op) {
|
|
1574
|
+
html += '<div class="glass" style="display:flex;align-items:center;gap:8px;padding:8px 14px;margin-bottom:12px;border-radius:var(--radius-sm);flex-wrap:wrap">';
|
|
1575
|
+
html += '<button class="btn primary" onclick="showRegisterModal(\'' + b._url + '\')">Register</button>';
|
|
1576
|
+
html += '<button class="btn danger" onclick="showDeregisterModal(\'' + b._url + '\')">Deregister</button>';
|
|
1577
|
+
html += '<button class="btn" onclick="showSendModal(\'' + b._url + '\')">Send</button>';
|
|
1578
|
+
html += '<button class="btn" onclick="showConnectModal(\'' + b._url + '\')">Connect Peer</button>';
|
|
1579
|
+
html += '<button class="btn" onclick="openLogViewer(\'' + b._url + '\')">View Logs</button>';
|
|
1580
|
+
html += '<button class="btn" onclick="logoutOperator(\'' + b._url + '\')">Logout</button>';
|
|
1581
|
+
html += '</div>';
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1282
1584
|
html += '<div class="cards-grid">';
|
|
1283
1585
|
|
|
1284
1586
|
// Bridge info card
|
|
1285
|
-
html += '<div class="detail-card glass"><h3>Bridge</h3>';
|
|
1587
|
+
html += '<div class="detail-card glass" onclick="openCardOverlay(\'bridge\')"><h3>Bridge <span style="font-size:9px;color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">— click to expand</span></h3>';
|
|
1286
1588
|
html += '<div class="panel-row"><span class="label">Status</span><span class="value ' + color + '">' + statusText + '</span></div>';
|
|
1287
1589
|
html += '<div class="panel-row"><span class="label">Pubkey</span><span class="value">' + truncPubkey(b.bridge.pubkeyHex) + '</span></div>';
|
|
1288
1590
|
if (op) {
|
|
@@ -1296,17 +1598,16 @@ function renderOverviewTab() {
|
|
|
1296
1598
|
html += '</div>';
|
|
1297
1599
|
|
|
1298
1600
|
// Network card
|
|
1299
|
-
html += '<div class="detail-card glass"><h3>Network</h3>';
|
|
1601
|
+
html += '<div class="detail-card glass" onclick="openCardOverlay(\'network\')"><h3>Network <span style="font-size:9px;color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">— click to expand</span></h3>';
|
|
1300
1602
|
html += '<div class="panel-row"><span class="label">Best Height</span><span class="value green">' + fmtHeight(b.headers.bestHeight) + '</span></div>';
|
|
1301
|
-
html += '<div class="panel-row"><span class="label">
|
|
1302
|
-
html += '<div class="panel-row"><span class="label">
|
|
1603
|
+
html += '<div class="panel-row"><span class="label">BSV P2P</span><span class="value ' + (b.bsvNode && b.bsvNode.connected ? 'green' : 'red') + '">' + (b.bsvNode ? b.bsvNode.peers : 0) + ' peers</span></div>';
|
|
1604
|
+
html += '<div class="panel-row"><span class="label">Mesh</span><span class="value">' + b.peers.connected + ' bridges</span></div>';
|
|
1303
1605
|
html += '<div class="panel-row"><span class="label">Mempool</span><span class="value">' + b.txs.mempool + ' txs</span></div>';
|
|
1304
|
-
html += '<div class="panel-row"><span class="label">Seen</span><span class="value">' + b.txs.seen + ' txs</span></div>';
|
|
1305
1606
|
html += '</div>';
|
|
1306
1607
|
|
|
1307
1608
|
// Wallet card (operator)
|
|
1308
1609
|
if (op && b.wallet) {
|
|
1309
|
-
html += '<div class="detail-card glass"><h3>Wallet</h3>';
|
|
1610
|
+
html += '<div class="detail-card glass" onclick="openCardOverlay(\'wallet\')"><h3>Wallet <span style="font-size:9px;color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">— click to expand</span></h3>';
|
|
1310
1611
|
if (b.bridge.address) {
|
|
1311
1612
|
html += '<div class="panel-row"><span class="label">Address</span><span class="value" style="font-size:10px;word-break:break-all">' + b.bridge.address + '</span></div>';
|
|
1312
1613
|
html += '<div style="text-align:center;padding:10px 0"><img src="' + generateQR(b.bridge.address) + '" alt="QR" style="border-radius:6px" width="100" height="100"></div>';
|
|
@@ -1316,53 +1617,28 @@ function renderOverviewTab() {
|
|
|
1316
1617
|
html += '</div>';
|
|
1317
1618
|
}
|
|
1318
1619
|
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
html += '<div class="panel-row"><span class="label">Peers</span><span class="value">' + (b.bsvNode.peers || 0) + '</span></div>';
|
|
1324
|
-
html += '<div class="panel-row"><span class="label">Height</span><span class="value">' + (b.bsvNode.height ? b.bsvNode.height.toLocaleString() : '-') + '</span></div>';
|
|
1325
|
-
html += '</div>';
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
// Peers card
|
|
1329
|
-
html += '<div class="detail-card glass"><h3>Peers (' + b.peers.connected + ')</h3>';
|
|
1330
|
-
if (b.peers.list.length === 0) {
|
|
1620
|
+
// Peers card — only show connected peers
|
|
1621
|
+
const connectedPeers = b.peers.list.filter(p => p.connected);
|
|
1622
|
+
html += '<div class="detail-card glass" style="max-height:300px;overflow-y:auto" onclick="openCardOverlay(\'peers\')"><h3 style="display:flex;justify-content:space-between;align-items:center">Peers (' + connectedPeers.length + ') <span style="font-size:9px;color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">— click to expand</span><button class="btn ghost" style="font-size:9px;padding:2px 8px;text-transform:none;letter-spacing:0" onclick="event.stopPropagation();openModal(BOND_EXPLAINER)">What is BND?</button></h3>';
|
|
1623
|
+
if (connectedPeers.length === 0) {
|
|
1331
1624
|
html += '<div style="color:var(--text-dim);font-size:12px">No peers connected</div>';
|
|
1332
1625
|
} else {
|
|
1333
1626
|
const showAll = window._showAllPeers || false;
|
|
1334
|
-
const visiblePeers = showAll ?
|
|
1627
|
+
const visiblePeers = showAll ? connectedPeers : connectedPeers.slice(0, 4);
|
|
1335
1628
|
for (const p of visiblePeers) {
|
|
1336
|
-
|
|
1337
|
-
html += '<div class="peer-entry" style="cursor:pointer" onclick="this.classList.toggle(\'expanded\')"><div><span class="peer-dot ' + dotC + '"></span><span class="peer-name">' + truncPubkey(p.pubkeyHex) + '</span> <span style="color:var(--text-dim)">score ' + (p.score !== undefined ? p.score.toFixed(2) : '?') + '</span></div>';
|
|
1629
|
+
html += '<div class="peer-entry" style="cursor:pointer" onclick="event.stopPropagation();this.classList.toggle(\'expanded\')"><div><span class="peer-dot green"></span><span class="peer-name">' + truncPubkey(p.pubkeyHex) + '</span> <span style="color:var(--text-dim)">score ' + (p.score !== undefined ? p.score.toFixed(2) : '?') + '</span></div>';
|
|
1338
1630
|
html += '<div class="peer-meta">' + (p.endpoint || '') + (p.health ? ' · ' + p.health : '') + '</div>';
|
|
1339
1631
|
if (p.scoreBreakdown) html += '<div class="score-bars">' + renderScoreBars(p.scoreBreakdown) + '</div>';
|
|
1340
1632
|
html += '</div>';
|
|
1341
1633
|
}
|
|
1342
|
-
if (
|
|
1343
|
-
html += '<div style="text-align:center;padding:4px 0"><button class="btn ghost" style="font-size:11px;padding:2px 10px" onclick="window._showAllPeers=!window._showAllPeers;renderActiveTab()">' + (showAll ? 'Show less' : 'Show all ' +
|
|
1634
|
+
if (connectedPeers.length > 4) {
|
|
1635
|
+
html += '<div style="text-align:center;padding:4px 0"><button class="btn ghost" style="font-size:11px;padding:2px 10px" onclick="event.stopPropagation();window._showAllPeers=!window._showAllPeers;renderActiveTab()">' + (showAll ? 'Show less' : 'Show all ' + connectedPeers.length + ' peers') + '</button></div>';
|
|
1344
1636
|
}
|
|
1345
1637
|
}
|
|
1346
1638
|
html += '</div>';
|
|
1347
1639
|
|
|
1348
|
-
// Actions card (operator)
|
|
1349
|
-
if (op) {
|
|
1350
|
-
html += '<div class="detail-card glass"><h3>Actions</h3>';
|
|
1351
|
-
html += '<div class="actions-grid">';
|
|
1352
|
-
html += '<button class="btn primary" onclick="showRegisterModal(\'' + b._url + '\')">Register</button>';
|
|
1353
|
-
html += '<button class="btn danger" onclick="showDeregisterModal(\'' + b._url + '\')">Deregister</button>';
|
|
1354
|
-
html += '<button class="btn" onclick="showSendModal(\'' + b._url + '\')">Send</button>';
|
|
1355
|
-
html += '<button class="btn" onclick="showConnectModal(\'' + b._url + '\')">Connect Peer</button>';
|
|
1356
|
-
html += '</div>';
|
|
1357
|
-
html += '<button class="btn ghost" onclick="openLogViewer(\'' + b._url + '\')" style="width:100%;margin-top:8px">View Live Logs</button>';
|
|
1358
|
-
if (isOperator(b._url)) html += '<button class="btn ghost" onclick="logoutOperator(\'' + b._url + '\')" style="width:100%;margin-top:4px;color:var(--text-dim)">Logout</button>';
|
|
1359
|
-
html += '<div style="margin-top:10px;padding:8px 10px;background:rgba(88,166,255,0.06);border:1px solid rgba(88,166,255,0.12);border-radius:var(--radius-sm);font-size:11px;color:var(--text-muted)">To backfill historical inscriptions and tokens, run <span style="font-family:var(--mono);color:var(--accent-blue)">relay-bridge backfill</span> from the CLI.</div>';
|
|
1360
|
-
html += '</div>';
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
1640
|
html += '</div>'; // cards-grid
|
|
1364
1641
|
|
|
1365
|
-
html += '<div style="color:var(--text-dim);font-size:10px;text-align:center;padding:12px 0">Updated ' + new Date().toLocaleTimeString() + '</div>';
|
|
1366
1642
|
return html;
|
|
1367
1643
|
}
|
|
1368
1644
|
|
|
@@ -2222,11 +2498,12 @@ function renderMiniMap(b) {
|
|
|
2222
2498
|
|
|
2223
2499
|
function renderScoreBars(breakdown) {
|
|
2224
2500
|
if (!breakdown) return '';
|
|
2225
|
-
const labels = [['UPT', breakdown.uptime], ['RTT', breakdown.responseTime], ['ACC', breakdown.dataAccuracy], ['
|
|
2501
|
+
const labels = [['UPT', breakdown.uptime], ['RTT', breakdown.responseTime], ['ACC', breakdown.dataAccuracy], ['BND', breakdown.stakeAge]];
|
|
2226
2502
|
let html = '<div class="score-bars">';
|
|
2227
2503
|
for (const [label, val] of labels) {
|
|
2228
2504
|
const pct = Math.round(val * 100);
|
|
2229
|
-
|
|
2505
|
+
const color = label === 'BND' ? (val >= 0.4 ? 'green' : val > 0 ? 'yellow' : 'yellow') : scoreColor(val);
|
|
2506
|
+
html += '<div class="score-row"><span class="score-label">' + label + '</span><div class="score-bar-bg"><div class="score-bar-fill ' + color + '" style="width:' + pct + '%"></div></div><span class="score-val">' + val.toFixed(2) + '</span></div>';
|
|
2230
2507
|
}
|
|
2231
2508
|
return html + '</div>';
|
|
2232
2509
|
}
|
package/lib/status-server.js
CHANGED
|
@@ -27,6 +27,7 @@ import { handlePostData, handleGetTopics, handleGetData } from './data-endpoints
|
|
|
27
27
|
|
|
28
28
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
29
29
|
const DASHBOARD_HTML = readFileSync(join(__dirname, '..', 'dashboard', 'index.html'), 'utf8')
|
|
30
|
+
const PKG_VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version
|
|
30
31
|
export class StatusServer {
|
|
31
32
|
/**
|
|
32
33
|
* @param {object} opts
|
|
@@ -121,6 +122,7 @@ export class StatusServer {
|
|
|
121
122
|
const status = {
|
|
122
123
|
bridge: {
|
|
123
124
|
name: this._config.name || null,
|
|
125
|
+
version: PKG_VERSION,
|
|
124
126
|
pubkeyHex: this._config.pubkeyHex || null,
|
|
125
127
|
meshId: this._config.meshId || null,
|
|
126
128
|
uptimeSeconds: Math.floor((Date.now() - this._startedAt) / 1000)
|