Files
neuron-tai/packages/tracker/meshnet_tracker/dashboard.html

198 lines
8.6 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>meshnet tracker</title>
<style>
:root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#c9d1d9;
--dim:#8b949e; --accent:#58a6ff; --ok:#3fb950; --bad:#f85149; --warn:#d29922; }
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--fg);
font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
header { display:flex; align-items:baseline; gap:14px; padding:14px 20px;
border-bottom:1px solid var(--border); }
header h1 { font-size:16px; margin:0; color:var(--accent); }
header .meta { color:var(--dim); font-size:12px; }
main { display:grid; grid-template-columns:repeat(auto-fit,minmax(340px,1fr));
gap:14px; padding:14px 20px; }
section { background:var(--panel); border:1px solid var(--border);
border-radius:8px; padding:12px 14px; min-height:80px; }
section h2 { margin:0 0 8px; font-size:12px; text-transform:uppercase;
letter-spacing:.08em; color:var(--dim); }
table { width:100%; border-collapse:collapse; font-size:12px; }
th { text-align:left; color:var(--dim); font-weight:normal;
border-bottom:1px solid var(--border); padding:2px 6px 4px 0; }
td { padding:3px 6px 3px 0; border-bottom:1px solid #21262d; }
.ok { color:var(--ok); } .bad { color:var(--bad); } .warn { color:var(--warn); }
.dim { color:var(--dim); } .num { text-align:right; }
a { color:var(--accent); text-decoration:none; }
.empty { color:var(--dim); font-style:italic; }
.pill { display:inline-block; padding:0 7px; border-radius:9px;
border:1px solid var(--border); font-size:11px; }
</style>
</head>
<body>
<header>
<h1>meshnet tracker</h1>
<span class="meta" id="self-url"></span>
<span class="meta" id="refreshed"></span>
</header>
<main>
<section><h2>Tracker hive</h2><div id="hive" class="empty">loading…</div></section>
<section><h2>Nodes &amp; coverage</h2><div id="nodes" class="empty">loading…</div></section>
<section><h2>Client balances</h2><div id="clients" class="empty">loading…</div></section>
<section><h2>Node pending payouts</h2><div id="pending" class="empty">loading…</div></section>
<section><h2>Settlement history</h2><div id="settlements" class="empty">loading…</div></section>
<section><h2>Strikes / bans / forfeitures</h2><div id="fraud" class="empty">loading…</div></section>
<section><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
</main>
<script>
"use strict";
const $ = id => document.getElementById(id);
const esc = s => String(s).replace(/[&<>"]/g,
c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
const usdt = v => (Math.round(v * 1e6) / 1e6).toFixed(6);
const short = (s, n=14) => { s = String(s); return s.length > n ? s.slice(0, 6) + "…" + s.slice(-5) : s; };
async function fetchJson(path) {
try {
const r = await fetch(path);
if (!r.ok) return null;
return await r.json();
} catch { return null; }
}
function table(headers, rows) {
if (!rows.length) return '<div class="empty">nothing yet</div>';
return '<table><tr>' + headers.map(h => `<th>${esc(h)}</th>`).join("") + '</tr>'
+ rows.map(r => '<tr>' + r.map(c => `<td>${c}</td>`).join("") + '</tr>').join("")
+ '</table>';
}
function renderHive(raft) {
const role = raft && (raft.state || raft.role);
if (!raft || role === "standalone") {
$("hive").innerHTML = '<span class="pill">standalone</span> single tracker, no cluster';
return;
}
const cls = role === "leader" ? "ok" : "warn";
$("hive").innerHTML =
`role <span class="${cls}">${esc(role || "?")}</span> · term ${esc(raft.term ?? "?")}` +
`<br>leader: ${raft.leader ? esc(raft.leader) : '<span class="dim">unknown</span>'}` +
(raft.peers ? `<br>peers: ${esc(Array.isArray(raft.peers) ? raft.peers.join(", ") : raft.peers)}` : "");
}
function renderNodes(map) {
const nodes = (map && map.nodes) || [];
if (!nodes.length) {
$("nodes").innerHTML = '<div class="empty">no nodes registered</div>'; return;
}
const byModel = {};
for (const n of nodes) {
const key = n.model || n.hf_repo || "?";
(byModel[key] = byModel[key] || []).push(n);
}
let html = "";
for (const [model, group] of Object.entries(byModel)) {
html += `<div><b>${esc(model)}</b> <span class="dim">(${group.length} node${group.length===1?"":"s"})</span></div>`;
html += table(["node", "shard", "mode", "health"], group.map(n => [
esc(short(n.node_id || "?")),
esc(`${n.shard_start ?? "?"}-${n.shard_end ?? "?"}`),
n.tracker_mode ? '<span class="pill">tracker</span>' : '<span class="dim">worker</span>',
(n.stats && (n.stats.alive === false || n.stats.healthy === false))
? '<span class="bad">down</span>' : '<span class="ok">up</span>',
]));
}
$("nodes").innerHTML = html;
}
function renderBilling(summary) {
if (!summary) {
$("clients").innerHTML = '<div class="empty">billing disabled</div>';
$("pending").innerHTML = '<div class="empty">billing disabled</div>';
return summary;
}
const clients = Object.entries(summary.clients || {});
$("clients").innerHTML = table(["api key", "balance (USDT)"],
clients.map(([k, v]) => [esc(short(k)),
`<span class="num ${v > 0 ? "ok" : "bad"}">${usdt(v)}</span>`]));
const pending = Object.entries(summary.node_pending || {});
const cut = summary.protocol_cut || 0;
$("pending").innerHTML =
table(["node wallet", "pending (USDT)"],
pending.map(([w, v]) => [esc(short(w)), `<span class="num">${usdt(v)}</span>`]))
+ `<div style="margin-top:6px" class="dim">protocol cut: <b>${usdt(cut)}</b> USDT</div>`;
return summary;
}
function renderSettlements(data) {
if (!data || !data.settlements) {
$("settlements").innerHTML = '<div class="empty">billing disabled</div>'; return;
}
const rows = data.settlements.slice(-15).reverse().map(s => {
const total = (s.payouts || []).reduce((a, p) => a + p.amount, 0);
const sig = s.signature;
const link = sig && !sig.startsWith("fake-") && !sig.startsWith("local-")
? `<a target="_blank" href="https://explorer.solana.com/tx/${encodeURIComponent(sig)}?cluster=devnet">${esc(short(sig))}</a>`
: (sig ? esc(short(sig)) : '<span class="warn">unconfirmed</span>');
return [new Date(s.ts * 1000).toLocaleTimeString(), String((s.payouts || []).length),
`<span class="num">${usdt(total)}</span>`, link];
});
$("settlements").innerHTML = table(["time", "payouts", "total (USDT)", "tx"], rows);
}
function renderFraud(wallets, summary) {
const rows = Object.entries((wallets && wallets.wallets) || {})
.filter(([, w]) => w.strike_count > 0 || w.banned)
.map(([addr, w]) => [esc(short(addr)), String(w.strike_count),
w.banned ? '<span class="bad">banned</span>' : '<span class="ok">active</span>']);
let html = rows.length ? table(["wallet", "strikes", "status"], rows)
: '<div class="empty">no strikes recorded</div>';
const forfeits = (summary && summary.forfeits) || [];
if (forfeits.length) {
html += '<div style="margin-top:6px"><b class="dim">recent forfeitures</b></div>'
+ table(["wallet", "amount", "reason"], forfeits.slice(-8).reverse().map(f =>
[esc(short(f.wallet)), `<span class="num bad">-${usdt(f.amount)}</span>`, esc(f.reason)]));
}
$("fraud").innerHTML = html;
}
function renderStats(stats) {
const models = (stats && (stats.models || stats.stats)) || stats;
if (!models || typeof models !== "object" || !Object.keys(models).length) {
$("stats").innerHTML = '<div class="empty">no requests yet</div>'; return;
}
const rows = Object.entries(models).map(([m, s]) => [
esc(m),
esc(String(s.rpm_last_hour ?? "?")),
esc(String(s.rpm_last_day ?? "?")),
esc(String(s.rpm_last_month ?? "?")),
]);
$("stats").innerHTML = table(["model", "rpm (1h)", "rpm (24h)", "rpm (30d)"], rows);
}
async function refresh() {
$("self-url").textContent = location.host;
const [raft, map, summary, settlements, wallets, stats] = await Promise.all([
fetchJson("/v1/raft/status"),
fetchJson("/v1/network/map"),
fetchJson("/v1/billing/summary"),
fetchJson("/v1/billing/settlements"),
fetchJson("/v1/registry/wallets"),
fetchJson("/v1/stats"),
]);
renderHive(raft);
renderNodes(map);
renderBilling(summary);
renderSettlements(settlements);
renderFraud(wallets, summary);
renderStats(stats);
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
}
refresh();
setInterval(refresh, 4000);
</script>
</body>
</html>