swactor/crates/runtime-dashboard/static/js/shared.js
Developer 548396ba4a feat: in-browser runtime, WebSocket gateway, dashboard improvements
- NodeAddr abstraction replacing SocketAddr across distribution crate (~30 files)
- Core wasm32 compatibility: web-time, cfg-gated threads, wire encoding extraction
- WebSocket transport (browser) with GatewayControl protocol
- WebSocket gateway (native) with session routing and control protocol
- BrowserRuntime API: JS actor support, connect/spawn/send/tick
- Dashboard: extracted HTML to static files, gossip protocol panel with
  membership event log, visual fixes, click-to-filter, worker-colored rows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:51:36 +00:00

159 lines
5.8 KiB
JavaScript

/* Swactor Dashboard — Shared JavaScript */
// ── Mode Detection ─────────────────────────────────────────────
var DASHBOARD_MODE = (document.querySelector('meta[name="dashboard-mode"]') || {}).content || 'live';
var isReplay = (DASHBOARD_MODE === 'replay');
// ── Shared DOM References ──────────────────────────────────────
var statusDot = document.getElementById('statusDot');
var uptimeLabel = document.getElementById('uptimeLabel');
var replayBadge = document.getElementById('replayBadge');
var replaySpeed = document.getElementById('replaySpeed');
var replayPct = document.getElementById('replayPct');
var progressBarWrap = document.getElementById('progressBarWrap');
var progressFill = document.getElementById('progressFill');
// ── Replay UI Setup ────────────────────────────────────────────
if (isReplay) {
if (replayBadge) replayBadge.className = 'replay-badge visible';
if (progressBarWrap) progressBarWrap.className = 'progress-bar-wrap visible';
if (statusDot) statusDot.className = 'status-dot replaying';
if (uptimeLabel) uptimeLabel.style.display = 'none';
}
// ── Uptime Tracking ────────────────────────────────────────────
var lastUptimeMs = null;
var lastStatsTime = null;
function updateUptime() {
if (isReplay || !uptimeLabel) return;
var up = lastUptimeMs;
if (up !== null && lastStatsTime !== null) {
up += (Date.now() - lastStatsTime);
}
if (up === null) { uptimeLabel.textContent = ''; return; }
var s = Math.floor(up / 1000);
var d = Math.floor(s / 86400);
var h = Math.floor((s % 86400) / 3600);
var m = Math.floor((s % 3600) / 60);
var sec = s % 60;
var parts = [];
if (d > 0) parts.push(d + 'd');
if (h > 0 || d > 0) parts.push(h + 'h');
parts.push(m + 'm');
parts.push(sec + 's');
uptimeLabel.textContent = parts.join(' ');
}
if (uptimeLabel) setInterval(updateUptime, 1000);
function trackUptime(data) {
if (typeof data.uptime_ms === 'number') {
lastUptimeMs = data.uptime_ms;
lastStatsTime = Date.now();
}
}
// ── Status Dot Management ──────────────────────────────────────
function setStatus(s) {
if (!statusDot) return;
if (s === 'done') {
statusDot.className = 'status-dot done';
if (isReplay) {
if (replayPct) replayPct.textContent = '100%';
if (progressFill) progressFill.style.width = '100%';
}
} else if (s === 'disconnected') {
statusDot.className = 'status-dot disconnected';
} else {
statusDot.className = isReplay ? 'status-dot replaying' : 'status-dot';
}
}
// ── Utility Functions ──────────────────────────────────────────
function escapeHtml(s) {
if (!s) return '';
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function addrToHex(addr) {
var bytes = Array.isArray(addr) ? addr : Object.values(addr);
var hex = '';
for (var j = 0; j < Math.min(8, bytes.length); j++) {
hex += ('0' + bytes[j].toString(16)).slice(-2);
}
return hex + '\u2026';
}
function addrToFullHex(addr) {
var bytes = Array.isArray(addr) ? addr : Object.values(addr);
var hex = '';
for (var j = 0; j < bytes.length; j++) {
hex += ('0' + bytes[j].toString(16)).slice(-2);
}
return hex;
}
function shortTypeName(full) {
if (!full) return '';
var parts = full.split('::');
return parts[parts.length - 1];
}
// ── Color Palette ──────────────────────────────────────────────
var colors = ['#4caf50','#2196f3','#ff9800','#f44336','#9c27b0','#00bcd4','#ffeb3b','#e91e63'];
function colorWithAlpha(hex, alpha) {
var r = parseInt(hex.slice(1, 3), 16);
var g = parseInt(hex.slice(3, 5), 16);
var b = parseInt(hex.slice(5, 7), 16);
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
}
// ── SSE Connection Factory ─────────────────────────────────────
//
// Usage:
// var es = createSSE({
// stats: function(data) { ... },
// activity: function(data) { ... },
// distribution: function(data) { ... }
// });
//
function createSSE(handlers) {
var es = new EventSource('/events');
// Wire page-specific handlers
Object.keys(handlers).forEach(function(event) {
es.addEventListener(event, function(e) {
try { handlers[event](JSON.parse(e.data)); }
catch(err) { console.error(event + ' parse error', err); }
});
});
// Common replay handlers
es.addEventListener('replay_meta', function(e) {
try {
var meta = JSON.parse(e.data);
if (replaySpeed) { replaySpeed.textContent = meta.speed + 'x'; replaySpeed.style.display = 'inline'; }
if (replayPct) { replayPct.style.display = 'inline'; replayPct.textContent = '0%'; }
} catch(err) { console.error('replay_meta parse error', err); }
});
es.addEventListener('replay_progress', function(e) {
try {
var data = JSON.parse(e.data);
var pct = Math.round(data.progress * 100);
if (replayPct) replayPct.textContent = pct + '%';
if (progressFill) progressFill.style.width = pct + '%';
} catch(err) { console.error('replay_progress parse error', err); }
});
es.addEventListener('done', function() {
setStatus('done');
es.close();
});
es.onerror = function() { setStatus('disconnected'); };
es.onopen = function() { setStatus('connected'); };
return es;
}