160 lines
5.8 KiB
JavaScript
160 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|