231 lines
8.2 KiB
Rust
231 lines
8.2 KiB
Rust
|
|
pub const ACTOR_DETAIL_HTML: &str = r##"<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>Actor Detail — Swactor Dashboard</title>
|
||
|
|
<style>
|
||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
|
body { font-family: 'Menlo', 'Consolas', 'Monaco', monospace; background: #0f1117; color: #e0e0e0; font-size: 13px; }
|
||
|
|
|
||
|
|
.header {
|
||
|
|
display: flex; align-items: center; justify-content: space-between;
|
||
|
|
padding: 12px 20px; background: #161822; border-bottom: 1px solid #2a2d3e;
|
||
|
|
}
|
||
|
|
.header h1 { font-size: 16px; font-weight: 600; color: #fff; }
|
||
|
|
.status-dot {
|
||
|
|
width: 10px; height: 10px; border-radius: 50%; background: #4caf50;
|
||
|
|
display: inline-block; margin-left: 8px; vertical-align: middle;
|
||
|
|
}
|
||
|
|
.status-dot.disconnected { background: #f44336; }
|
||
|
|
|
||
|
|
.header-left { display: flex; align-items: center; }
|
||
|
|
.nav-links { display: flex; gap: 4px; margin-left: 20px; }
|
||
|
|
.nav-link {
|
||
|
|
color: #888; text-decoration: none; font-size: 12px;
|
||
|
|
padding: 4px 10px; border-radius: 3px; transition: color 0.2s;
|
||
|
|
}
|
||
|
|
.nav-link:hover { color: #e0e0e0; }
|
||
|
|
.nav-link.active { color: #fff; background: #2a2d3e; }
|
||
|
|
|
||
|
|
.content { padding: 16px 20px; max-width: 900px; }
|
||
|
|
|
||
|
|
.breadcrumb { color: #555; font-size: 12px; margin-bottom: 12px; }
|
||
|
|
.breadcrumb a { color: #888; text-decoration: none; }
|
||
|
|
.breadcrumb a:hover { color: #e0e0e0; }
|
||
|
|
|
||
|
|
.info-card {
|
||
|
|
background: #161822; border: 1px solid #2a2d3e; border-radius: 6px;
|
||
|
|
padding: 16px; margin-bottom: 12px;
|
||
|
|
}
|
||
|
|
.info-row { display: flex; gap: 24px; margin-bottom: 6px; flex-wrap: wrap; }
|
||
|
|
.info-label { color: #888; font-size: 11px; text-transform: uppercase; }
|
||
|
|
.info-value { color: #fff; font-weight: 700; font-size: 15px; }
|
||
|
|
.info-value.healthy { color: #4caf50; }
|
||
|
|
.info-value.poisoned { color: #f44336; }
|
||
|
|
|
||
|
|
.stats-cards {
|
||
|
|
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
|
||
|
|
margin-bottom: 12px;
|
||
|
|
}
|
||
|
|
.stat-card {
|
||
|
|
background: #161822; border: 1px solid #2a2d3e; border-radius: 4px;
|
||
|
|
padding: 12px; text-align: center;
|
||
|
|
}
|
||
|
|
.stat-card .value { font-size: 22px; font-weight: 700; color: #fff; }
|
||
|
|
.stat-card .label { font-size: 10px; color: #888; text-transform: uppercase; margin-top: 2px; }
|
||
|
|
|
||
|
|
.sparkline-panel {
|
||
|
|
background: #161822; border: 1px solid #2a2d3e; border-radius: 6px;
|
||
|
|
padding: 14px; margin-bottom: 12px;
|
||
|
|
}
|
||
|
|
.sparkline-panel h3 { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
||
|
|
.sparkline-panel svg { width: 100%; height: 50px; }
|
||
|
|
|
||
|
|
::-webkit-scrollbar { width: 6px; }
|
||
|
|
::-webkit-scrollbar-track { background: #0f1117; }
|
||
|
|
::-webkit-scrollbar-thumb { background: #2a2d3e; border-radius: 3px; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="header">
|
||
|
|
<div class="header-left">
|
||
|
|
<h1>
|
||
|
|
Swactor Runtime Dashboard
|
||
|
|
<span id="statusDot" class="status-dot"></span>
|
||
|
|
</h1>
|
||
|
|
<nav class="nav-links">
|
||
|
|
<a href="/" class="nav-link">Overview</a>
|
||
|
|
<a href="/actors" class="nav-link">Actors</a>
|
||
|
|
</nav>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="content">
|
||
|
|
<div class="breadcrumb">
|
||
|
|
<a href="/">Overview</a> / <a href="/actors">Actors</a> / <span id="addrBreadcrumb">—</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="info-card">
|
||
|
|
<div class="info-row">
|
||
|
|
<div><div class="info-label">Address</div><div class="info-value" id="addrFull">—</div></div>
|
||
|
|
<div><div class="info-label">Worker</div><div class="info-value" id="addrWorker">—</div></div>
|
||
|
|
<div><div class="info-label">Status</div><div class="info-value" id="addrStatus">—</div></div>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<div><div class="info-label">Last Message Type</div><div class="info-value" id="addrLastMsg" style="color:#4caf50;font-size:13px;">—</div></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="stats-cards">
|
||
|
|
<div class="stat-card"><div class="value" id="addrMsgs">0</div><div class="label">Messages</div></div>
|
||
|
|
<div class="stat-card"><div class="value" id="addrMailbox">0</div><div class="label">Mailbox</div></div>
|
||
|
|
<div class="stat-card"><div class="value" id="addrRate">0</div><div class="label">Msg/s</div></div>
|
||
|
|
<div class="stat-card"><div class="value" id="addrWorkerLoad">—</div><div class="label">Worker Load</div></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="sparkline-panel">
|
||
|
|
<h3>Message Rate</h3>
|
||
|
|
<svg id="rateSpark" viewBox="0 0 400 50" preserveAspectRatio="none"></svg>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="sparkline-panel">
|
||
|
|
<h3>Mailbox Depth</h3>
|
||
|
|
<svg id="mboxSpark" viewBox="0 0 400 50" preserveAspectRatio="none"></svg>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
(function() {
|
||
|
|
var targetAddr = '__ACTOR_ADDR__';
|
||
|
|
var dot = document.getElementById('statusDot');
|
||
|
|
|
||
|
|
var history = { rates: [], mailbox: [], prev_msgs: 0 };
|
||
|
|
|
||
|
|
function formatAddr(addr) {
|
||
|
|
if (!addr) return '';
|
||
|
|
var bytes = Array.isArray(addr) ? addr : Object.values(addr);
|
||
|
|
var hex = '';
|
||
|
|
for (var i = 0; i < bytes.length; i++) {
|
||
|
|
hex += ('0' + bytes[i].toString(16)).slice(-2);
|
||
|
|
}
|
||
|
|
return hex;
|
||
|
|
}
|
||
|
|
|
||
|
|
function shortAddr(hex) {
|
||
|
|
return hex.length > 16 ? hex.substring(0, 16) + '\u2026' : hex;
|
||
|
|
}
|
||
|
|
|
||
|
|
function shortTypeName(full) {
|
||
|
|
if (!full) return '\u2014';
|
||
|
|
var parts = full.split('::');
|
||
|
|
return parts[parts.length - 1];
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateSparklineSvg(svgEl, data, color) {
|
||
|
|
if (!data || data.length < 2) { svgEl.innerHTML = ''; return; }
|
||
|
|
var max = Math.max.apply(null, data);
|
||
|
|
if (max === 0) max = 1;
|
||
|
|
var w = 400, h = 50;
|
||
|
|
var step = w / (data.length - 1);
|
||
|
|
var points = data.map(function(v, i) {
|
||
|
|
return (i * step).toFixed(1) + ',' + (h - (v / max) * (h - 4) - 2).toFixed(1);
|
||
|
|
}).join(' ');
|
||
|
|
svgEl.innerHTML = '<polyline fill="none" stroke="' + color + '" stroke-width="2" points="' + points + '"/>';
|
||
|
|
}
|
||
|
|
|
||
|
|
function findActor(stats) {
|
||
|
|
if (!stats.actor_details) return null;
|
||
|
|
for (var i = 0; i < stats.actor_details.length; i++) {
|
||
|
|
var a = stats.actor_details[i];
|
||
|
|
var hex = formatAddr(a.address);
|
||
|
|
if (hex === targetAddr || hex.indexOf(targetAddr) === 0) return a;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateDetail(stats) {
|
||
|
|
var actor = findActor(stats);
|
||
|
|
if (!actor) return;
|
||
|
|
|
||
|
|
var hex = formatAddr(actor.address);
|
||
|
|
document.getElementById('addrBreadcrumb').textContent = shortAddr(hex);
|
||
|
|
document.getElementById('addrFull').textContent = hex;
|
||
|
|
document.getElementById('addrWorker').textContent = 'W' + actor.worker_id;
|
||
|
|
|
||
|
|
var statusEl = document.getElementById('addrStatus');
|
||
|
|
if (actor.poisoned) {
|
||
|
|
statusEl.textContent = 'POISONED';
|
||
|
|
statusEl.className = 'info-value poisoned';
|
||
|
|
} else {
|
||
|
|
statusEl.textContent = 'Healthy';
|
||
|
|
statusEl.className = 'info-value healthy';
|
||
|
|
}
|
||
|
|
|
||
|
|
document.getElementById('addrLastMsg').textContent = shortTypeName(actor.last_msg_type);
|
||
|
|
document.getElementById('addrMsgs').textContent = actor.messages_processed.toLocaleString();
|
||
|
|
document.getElementById('addrMailbox').textContent = actor.mailbox_depth;
|
||
|
|
|
||
|
|
// Compute rate
|
||
|
|
var rate = actor.messages_processed - history.prev_msgs;
|
||
|
|
if (rate < 0) rate = 0;
|
||
|
|
history.prev_msgs = actor.messages_processed;
|
||
|
|
history.rates.push(rate);
|
||
|
|
history.mailbox.push(actor.mailbox_depth);
|
||
|
|
if (history.rates.length > 300) { history.rates.shift(); history.mailbox.shift(); }
|
||
|
|
|
||
|
|
// Rate per second (SSE interval is ~200ms, so multiply by 5)
|
||
|
|
document.getElementById('addrRate').textContent = (rate * 5).toLocaleString();
|
||
|
|
|
||
|
|
// Worker load
|
||
|
|
if (stats.workers) {
|
||
|
|
var w = stats.workers.find(function(w) { return w.id === actor.worker_id; });
|
||
|
|
if (w) {
|
||
|
|
document.getElementById('addrWorkerLoad').textContent =
|
||
|
|
w.num_actors + ' actors, mbox ' + w.mailbox_depth;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
updateSparklineSvg(document.getElementById('rateSpark'), history.rates, '#4caf50');
|
||
|
|
updateSparklineSvg(document.getElementById('mboxSpark'), history.mailbox, '#2196f3');
|
||
|
|
}
|
||
|
|
|
||
|
|
var es = new EventSource('/events');
|
||
|
|
|
||
|
|
es.addEventListener('stats', function(e) {
|
||
|
|
try { updateDetail(JSON.parse(e.data)); } catch(err) { console.error(err); }
|
||
|
|
});
|
||
|
|
|
||
|
|
es.addEventListener('done', function() {
|
||
|
|
dot.className = 'status-dot disconnected';
|
||
|
|
es.close();
|
||
|
|
});
|
||
|
|
|
||
|
|
es.onerror = function() { dot.className = 'status-dot disconnected'; };
|
||
|
|
es.onopen = function() { dot.className = 'status-dot'; };
|
||
|
|
})();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
"##;
|