(function() { var C = window.ExplorerCommon; /* ── State ──────────────────────────────────────────────────────────────── */ var state = { txSeries: [], // tx counts per recent block (oldest → newest) intervalSeries: [], // block intervals in seconds supplyHistory: [], // cumulative tx count (proxy sparkline for supply) tpsSeries: [], // same as txSeries, used for strip micro-bar }; /* ── Address / Block links ──────────────────────────────────────────────── */ function linkAddress(value, label) { return '' + C.esc(label || value) + ''; } function linkBlock(index) { return '#' + C.esc(index) + ''; } /* ── Micro chart helpers (strip sparklines) ─────────────────────────────── */ function resizeMicro(canvas) { var dpr = window.devicePixelRatio || 1; var w = canvas.offsetWidth || 80; var h = canvas.offsetHeight || 36; canvas.width = Math.max(1, Math.floor(w * dpr)); canvas.height = Math.max(1, Math.floor(h * dpr)); var ctx = canvas.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); return { ctx: ctx, w: w, h: h }; } /** Tiny bar chart for the TPS strip cell */ function drawMicroBars(canvas, values, color) { if (!canvas) return; var r = resizeMicro(canvas); var ctx = r.ctx; var w = r.w; var h = r.h; ctx.clearRect(0, 0, w, h); if (!values || !values.length) return; var n = Math.min(values.length, 24); var vs = values.slice(-n); var max = Math.max.apply(null, vs.concat([1])); var gap = 2; var bw = Math.max(2, (w - gap * (n + 1)) / n); ctx.fillStyle = color; for (var i = 0; i < vs.length; i++) { var bh = Math.max(2, (vs[i] / max) * (h - 4)); var x = gap + i * (bw + gap); var y = h - bh; ctx.fillRect(x, y, bw, bh); } } /** Tiny sparkline with gradient fill for the Supply strip cell */ function drawSparkline(canvas, values, color) { if (!canvas) return; var r = resizeMicro(canvas); var ctx = r.ctx; var w = r.w; var h = r.h; ctx.clearRect(0, 0, w, h); if (!values || values.length < 2) { // draw a flat line when no data ctx.strokeStyle = color + '55'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(2, h / 2); ctx.lineTo(w - 2, h / 2); ctx.stroke(); return; } var n = Math.min(values.length, 24); var vs = values.slice(-n); var mn = Math.min.apply(null, vs); var mx = Math.max.apply(null, vs); if (mn === mx) { mn = mn - 1; mx = mx + 1; } var span = mx - mn; var pad = 3; var pts = []; for (var i = 0; i < vs.length; i++) { var x = pad + (i / (vs.length - 1)) * (w - pad * 2); var y = (h - pad) - ((vs[i] - mn) / span) * (h - pad * 2); pts.push([x, y]); } // gradient fill var grad = ctx.createLinearGradient(0, 0, 0, h); grad.addColorStop(0, color + '40'); grad.addColorStop(1, color + '05'); ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]); for (var j = 1; j < pts.length; j++) ctx.lineTo(pts[j][0], pts[j][1]); ctx.lineTo(pts[pts.length - 1][0], h); ctx.lineTo(pts[0][0], h); ctx.closePath(); ctx.fillStyle = grad; ctx.fill(); // line ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]); for (var k = 1; k < pts.length; k++) ctx.lineTo(pts[k][0], pts[k][1]); ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.stroke(); } /* ── Full-size chart helpers (main chart panels) ────────────────────────── */ function resizeCanvas(canvas) { var dpr = window.devicePixelRatio || 1; var w = canvas.clientWidth; var h = canvas.clientHeight; canvas.width = Math.max(1, Math.floor(w * dpr)); canvas.height = Math.max(1, Math.floor(h * dpr)); var ctx = canvas.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); return ctx; } function chartLayout(width, height) { return { left: 44, right: 10, top: 10, bottom: 26, plotW: Math.max(10, width - 54), plotH: Math.max(10, height - 36) }; } function fmtTick(v) { if (Math.abs(v) >= 1000) return String(Math.round(v)); if (Math.abs(v) >= 10) return (Math.round(v * 10) / 10).toFixed(1); return (Math.round(v * 100) / 100).toFixed(2); } function drawAxes(ctx, w, h, b, minY, maxY) { ctx.strokeStyle = '#334155'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(b.left, b.top); ctx.lineTo(b.left, h - b.bottom); ctx.stroke(); ctx.beginPath(); ctx.moveTo(b.left, h - b.bottom); ctx.lineTo(w - b.right, h - b.bottom); ctx.stroke(); ctx.fillStyle = '#94a3b8'; ctx.font = '11px "Inter", sans-serif'; for (var i = 0; i <= 4; i++) { var ratio = i / 4; var y = b.top + b.plotH * ratio; var val = maxY - (maxY - minY) * ratio; ctx.beginPath(); ctx.moveTo(b.left - 4, y); ctx.lineTo(b.left, y); ctx.stroke(); ctx.fillText(fmtTick(val), 4, y + 3); } ctx.fillText('oldest', b.left, h - 8); ctx.fillText('newest', w - b.right - 36, h - 8); } function drawBars(canvas, values, color) { if (!canvas) return; var ctx = resizeCanvas(canvas); var w = canvas.clientWidth; var h = canvas.clientHeight; ctx.clearRect(0, 0, w, h); var b = chartLayout(w, h); var max = Math.max.apply(null, (values || []).concat([1])); drawAxes(ctx, w, h, b, 0, max); if (!values || !values.length) return; var gap = 5; var bw = Math.max(2, (b.plotW - gap * (values.length + 1)) / values.length); ctx.fillStyle = color; for (var i = 0; i < values.length; i++) { var bh = max > 0 ? (values[i] / max) * b.plotH : 2; var x = b.left + gap + i * (bw + gap); var y = b.top + b.plotH - bh; ctx.fillRect(x, y, bw, bh); } } function drawLine(canvas, values, color) { if (!canvas) return; var ctx = resizeCanvas(canvas); var w = canvas.clientWidth; var h = canvas.clientHeight; ctx.clearRect(0, 0, w, h); var b = chartLayout(w, h); if (!values || !values.length) { drawAxes(ctx, w, h, b, 0, 1); return; } var mn = Math.min.apply(null, values); var mx = Math.max.apply(null, values); if (mn === mx) { mn = mn - 1; mx = mx + 1; } drawAxes(ctx, w, h, b, mn, mx); var span = mx - mn; ctx.beginPath(); for (var i = 0; i < values.length; i++) { var x = values.length === 1 ? b.left + b.plotW / 2 : b.left + (i * b.plotW) / (values.length - 1); var ratio = span === 0 ? 0.5 : (values[i] - mn) / span; var y = b.top + b.plotH - ratio * b.plotH; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke(); } function renderCharts() { drawBars(document.getElementById('txChart'), state.txSeries, '#2563eb'); drawLine(document.getElementById('intervalChart'), state.intervalSeries, '#0f766e'); } function renderStripCharts() { drawSparkline(document.getElementById('supplyChart'), state.supplyHistory, '#7db5ff'); drawMicroBars(document.getElementById('tpsChart'), state.tpsSeries, '#41c98a'); } /* ── renderStats ────────────────────────────────────────────────────────── */ function renderStats(net, blocks) { // Supply strip document.getElementById('sSupply').textContent = C.toTokenShort(net.total_supply); // Height + last block time document.getElementById('sHeight').textContent = Number(net.total_blocks || 0); if (blocks.length) { document.getElementById('sLastTime').textContent = C.timeAgo(blocks[0].time); } else { document.getElementById('sLastTime').textContent = '—'; } if (!blocks.length) { document.getElementById('sTps').textContent = '—'; document.getElementById('sBlockTime').textContent = '—'; document.getElementById('sLastBlock').textContent = '—'; document.getElementById('sLastHash').textContent = '—'; document.getElementById('sTxs').textContent = net.total_txs || 0; document.getElementById('sTransfers').textContent = 'Transfers: ' + (net.total_transfers || 0); document.getElementById('sRelayProofs').textContent = net.total_relay_proofs || 0; document.getElementById('sRelayCount').textContent = (net.relay_count || 0) + ' relay nodes'; document.getElementById('sValidators').textContent = net.validator_count || 0; document.getElementById('sTpsWindow').textContent = 'no data yet'; state.txSeries = []; state.intervalSeries = []; state.supplyHistory = []; state.tpsSeries = []; renderCharts(); renderStripCharts(); return; } // Newest block var newest = blocks[0]; document.getElementById('sLastBlock').textContent = '#' + newest.index; document.getElementById('sLastHash').textContent = C.short(newest.hash, 24); // TPS over window var totalTx = 0; for (var i = 0; i < blocks.length; i++) totalTx += Number(blocks[i].tx_count || 0); var t0 = new Date(blocks[blocks.length - 1].time).getTime(); var t1 = new Date(blocks[0].time).getTime(); var secs = Math.max(1, (t1 - t0) / 1000); var tps = (totalTx / secs).toFixed(2); document.getElementById('sTps').textContent = tps; document.getElementById('sTpsWindow').textContent = 'window: ~' + Math.round(secs) + ' sec'; // Avg block interval var asc = blocks.slice().reverse(); var intervals = []; for (var j = 1; j < asc.length; j++) { var prev = new Date(asc[j - 1].time).getTime(); var cur = new Date(asc[j].time).getTime(); intervals.push(Math.max(0, (cur - prev) / 1000)); } var avg = 0; if (intervals.length) { var sum = 0; for (var k = 0; k < intervals.length; k++) sum += intervals[k]; avg = sum / intervals.length; } document.getElementById('sBlockTime').textContent = avg ? avg.toFixed(2) + 's' : '—'; // Card stats document.getElementById('sTxs').textContent = net.total_txs || 0; document.getElementById('sTransfers').textContent = 'Transfers: ' + (net.total_transfers || 0); document.getElementById('sRelayProofs').textContent = net.total_relay_proofs || 0; document.getElementById('sRelayCount').textContent = (net.relay_count || 0) + ' relay nodes'; document.getElementById('sValidators').textContent = net.validator_count || 0; // Series for charts state.txSeries = asc.map(function(b) { return Number(b.tx_count || 0); }); state.intervalSeries = intervals; // Cumulative tx sparkline (supply proxy — shows chain activity growth) var cum = 0; state.supplyHistory = asc.map(function(b) { cum += Number(b.tx_count || 0); return cum; }); state.tpsSeries = state.txSeries; renderCharts(); renderStripCharts(); } /* ── renderRecentBlocks ─────────────────────────────────────────────────── */ function renderRecentBlocks(blocks) { var tbody = document.getElementById('blocksBody'); if (!tbody) return; if (!blocks || !blocks.length) { tbody.innerHTML = 'No blocks yet.'; return; } var rows = ''; var shown = blocks.slice(0, 20); for (var i = 0; i < shown.length; i++) { var b = shown[i]; var val = b.validator || b.proposer || ''; var valShort = val ? C.shortAddress(val) : '—'; var valCell = val ? '' + C.esc(valShort) + '' : '—'; var fees = b.total_fees_ut != null ? C.toToken(Number(b.total_fees_ut)) : '—'; rows += '' + '' + linkBlock(b.index) + '' + '' + C.esc(C.short(b.hash, 20)) + '' + '' + valCell + '' + '' + C.esc(b.tx_count || 0) + '' + '' + C.esc(fees) + '' + '' + C.esc(C.timeAgo(b.time)) + '' + ''; } tbody.innerHTML = rows; C.refreshIcons(); } /* ── Block detail panel ─────────────────────────────────────────────────── */ function showBlockPanel(data) { var panel = document.getElementById('blockDetailPanel'); var raw = document.getElementById('blockRaw'); if (!panel || !raw) return; raw.textContent = JSON.stringify(data, null, 2); panel.style.display = ''; panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function hideBlockPanel() { var panel = document.getElementById('blockDetailPanel'); if (panel) panel.style.display = 'none'; } async function loadBlock(index) { C.setStatus('Loading block #' + index + '…', 'warn'); try { var b = await C.fetchJSON('/api/block/' + encodeURIComponent(index)); showBlockPanel(b); C.setStatus('Block #' + index + ' loaded.', 'ok'); } catch (e) { C.setStatus('Block load failed: ' + e.message, 'err'); } } /* ── Main refresh ───────────────────────────────────────────────────────── */ async function refreshDashboard() { try { var data = await Promise.all([ C.fetchJSON('/api/netstats'), C.fetchJSON('/api/blocks?limit=36'), ]); renderStats(data[0] || {}, data[1] || []); renderRecentBlocks(data[1] || []); C.setStatus('Updated at ' + new Date().toLocaleTimeString(), 'ok'); C.refreshIcons(); } catch (e) { C.setStatus('Refresh failed: ' + e.message, 'err'); } } /* ── Search ─────────────────────────────────────────────────────────────── */ function handleSearch() { var raw = (document.getElementById('searchInput').value || '').trim(); if (!raw) return; // Pure integer → block lookup if (/^\d+$/.test(raw)) { loadBlock(raw); return; } // Hex tx id (32 bytes = 64 chars) → tx page if (/^[0-9a-fA-F]{64}$/.test(raw)) { C.navAddress(raw); return; } // Short hex (16 chars) or partial → try as tx id if (/^[0-9a-fA-F]{16,63}$/.test(raw)) { C.navTx(raw); return; } // DC address or any remaining string → address page C.navAddress(raw); } /* ── Event wiring ───────────────────────────────────────────────────────── */ var searchBtn = document.getElementById('searchBtn'); if (searchBtn) searchBtn.addEventListener('click', handleSearch); var openNodeBtn = document.getElementById('openNodeBtn'); if (openNodeBtn) { openNodeBtn.addEventListener('click', function() { var raw = (document.getElementById('searchInput').value || '').trim(); if (raw && C.isPubKey(raw)) { window.location.href = '/node?node=' + encodeURIComponent(raw); } else { window.location.href = '/validators'; } }); } var searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') handleSearch(); }); } // Block link clicks inside the blocks table document.addEventListener('click', function(e) { var t = e.target; if (!t) return; var link = t.closest ? t.closest('a.block-link') : null; if (link) { e.preventDefault(); loadBlock(link.dataset.index); return; } }); // Redraw on resize (both full-size and strip charts) window.addEventListener('resize', function() { renderCharts(); renderStripCharts(); }); /* ── Boot ───────────────────────────────────────────────────────────────── */ // Polling fallback — active when SSE is unavailable or disconnected. var pollTimer = null; function startPolling() { if (pollTimer) return; pollTimer = setInterval(refreshDashboard, 10000); } function stopPolling() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } var bootPromise = refreshDashboard(); // SSE live feed — stop polling while connected; resume on disconnect. var sseConn = C.connectSSE({ connected: function() { stopPolling(); C.setStatus('● LIVE', 'ok'); }, block: function(/*ev*/) { // A new block was committed — refresh everything immediately. refreshDashboard(); }, error: function() { startPolling(); C.setStatus('Offline — polling every 10s', 'warn'); } }); // If SSE is not supported by the browser, fall back to polling immediately. if (!sseConn) startPolling(); // Handle /?block=N — navigated here from tx page block link var blockParam = C.q('block'); if (blockParam) { bootPromise.then(function() { loadBlock(blockParam); }); } })();