Files
neuron-tai/packages/tracker/meshnet_tracker/dashboard.html
Dobromir Popov 95d79a0a16 quantizations
2026-07-12 01:33:51 +03:00

2436 lines
92 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>meshnet tracker</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="alternate icon" href="/favicon.ico">
<style>
:root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#e6edf3;
--dim:#8b949e; --accent:#58a6ff; --ok:#3fb950; --bad:#f85149; --warn:#d29922;
--hover-bg:#10151d;
--chat-input-bg:#21262d; --chat-user-bg:#1f6feb; --chat-user-border:#388bfd;
--chat-error-bg:rgba(248,81,73,.08); --chat-error-border:rgba(248,81,73,.35);
--chat-error-fg:#ffa198; }
* { box-sizing:border-box; }
html, body { height:100%; }
body { margin:0; background:var(--bg); color:var(--fg);
font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
body.chat-tab-active { overflow:hidden; height:100dvh; display:flex; flex-direction:column; }
header { display:flex; align-items:baseline; gap:14px; padding:14px 20px;
border-bottom:1px solid var(--border); flex-shrink:0; }
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; }
body.chat-tab-active main {
flex:1; min-height:0; display:flex; flex-direction:column;
padding:0; gap:0; overflow:hidden;
}
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; }
input, button { font:inherit; color:var(--fg); background:var(--bg);
border:1px solid var(--border); border-radius:6px; padding:5px 8px; }
input { width:100%; margin-bottom:6px; }
button { cursor:pointer; color:var(--accent); }
button:hover { border-color:var(--accent); }
button.small { font-size:11px; padding:1px 7px; }
.form-row { display:flex; gap:8px; }
.form-row button { white-space:nowrap; }
.error-msg { color:var(--bad); font-size:12px; min-height:16px; }
.keybox { display:flex; flex-wrap:wrap; align-items:center; gap:6px;
position:relative;
word-break:break-all; background:var(--bg); border:1px solid var(--border);
border-radius:6px; padding:4px 8px; margin:4px 0; font-size:11px; }
.key-text { cursor:text; flex:1 1 auto; min-width:12rem; }
.copy-tooltip {
position:absolute; right:8px; top:-26px;
background:var(--panel); border:1px solid var(--border); color:var(--ok);
padding:2px 8px; border-radius:4px; font-size:11px;
pointer-events:none; z-index:1; white-space:nowrap;
}
.tabs { display:flex; gap:10px; margin-bottom:8px; }
.tabs a { color:var(--dim); cursor:pointer; }
.tabs a.active { color:var(--accent); border-bottom:1px solid var(--accent); }
.dashboard-tabs { display:flex; gap:10px; padding:10px 20px 0; border-bottom:1px solid var(--border); flex-shrink:0; }
.dashboard-tabs button { border:0; border-bottom:1px solid transparent; border-radius:0;
background:transparent; color:var(--dim); padding:5px 0 8px; }
.dashboard-tabs button.active { color:var(--accent); border-bottom-color:var(--accent); }
.wide { grid-column:1 / -1; }
section[hidden] { display:none !important; }
section.chat-section {
padding:0; border:0; border-radius:0; background:var(--bg); min-height:0;
}
body.chat-tab-active section.chat-section {
flex:1; display:flex !important; flex-direction:column; min-height:0;
}
.chat-app {
display:grid; grid-template-columns:260px minmax(0, 1fr); gap:0;
flex:1; min-height:0; overflow:hidden; background:var(--bg);
}
.chat-sidebar {
display:flex; flex-direction:column; min-height:0;
border-right:1px solid var(--border); background:var(--panel);
}
.chat-new-btn {
margin:12px; width:calc(100% - 24px); text-align:left;
border:1px solid var(--border); border-radius:8px; padding:10px 12px;
background:transparent; color:var(--fg);
}
.chat-new-btn:hover { background:var(--hover-bg); border-color:var(--accent); }
.chat-session-list {
flex:1; overflow:auto; padding:0 8px 12px; display:flex; flex-direction:column; gap:2px;
}
.chat-session-list.empty-state {
justify-content:center; align-items:center; color:var(--dim); font-style:italic;
padding:24px 12px;
}
.chat-session-item {
position:relative; display:block; width:100%; text-align:left;
padding:10px 32px 10px 12px; border:1px solid transparent; border-radius:8px;
background:transparent; color:var(--fg); cursor:pointer;
}
.chat-session-item:hover { background:var(--hover-bg); }
.chat-session-item.active { background:var(--hover-bg); border-color:var(--border); }
.chat-session-title {
font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.chat-session-meta {
margin-top:3px; font-size:11px; color:var(--dim);
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.chat-session-delete {
position:absolute; top:50%; right:6px; transform:translateY(-50%);
padding:2px 6px; min-width:0; border:0; border-radius:4px;
background:transparent; color:var(--dim); line-height:1.2; opacity:0;
}
.chat-session-item:hover .chat-session-delete,
.chat-session-item.active .chat-session-delete { opacity:1; }
.chat-session-delete:hover { color:var(--bad); background:var(--chat-error-bg); }
.chat-main { display:flex; flex-direction:column; min-height:0; min-width:0; color-scheme:dark; }
.chat-toolbar {
display:flex; gap:12px; align-items:center; flex-shrink:0;
padding:10px 16px; border-bottom:1px solid var(--border); background:var(--panel);
}
.chat-toolbar label {
display:flex; align-items:center; gap:8px; color:var(--dim); margin:0;
}
.chat-toolbar select {
min-width:280px; max-width:min(520px, 60vw);
color:var(--fg); background:var(--chat-input-bg); border:1px solid var(--border);
border-radius:6px; padding:6px 8px; font-size:13px;
}
.chat-status { color:var(--dim); font-size:12px; margin-left:auto; }
.chat-messages {
flex:1; overflow:auto; padding:24px 16px; min-height:0;
background:var(--bg); color:var(--fg);
}
.chat-messages-inner {
max-width:768px; margin:0 auto; display:flex; flex-direction:column; gap:20px;
}
.chat-messages.empty .chat-messages-inner {
min-height:100%; justify-content:center; align-items:center;
color:var(--dim); font-size:14px;
}
.chat-row { display:flex; width:100%; }
.chat-row.user { justify-content:flex-end; }
.chat-row.assistant, .chat-row.error { justify-content:flex-start; }
.chat-bubble {
max-width:85%; padding:12px 14px; border-radius:16px; line-height:1.55;
white-space:pre-wrap; word-break:break-word; font-size:14px; color:var(--fg);
}
.chat-bubble.user {
background:var(--chat-user-bg); border:1px solid var(--chat-user-border);
border-bottom-right-radius:4px; color:#f0f6fc;
}
.chat-bubble.assistant {
background:var(--panel); border:1px solid var(--border);
border-bottom-left-radius:4px; max-width:100%; color:var(--fg);
}
.chat-bubble.error {
background:var(--chat-error-bg); border:1px solid var(--chat-error-border);
color:var(--chat-error-fg); border-bottom-left-radius:4px;
}
.chat-bubble-stack { display:flex; flex-direction:column; gap:4px; max-width:100%; }
.chat-stream-stats {
font-size:11px; color:var(--dim); font-variant-numeric:tabular-nums;
padding:0 2px; min-height:14px;
}
.chat-bubble.assistant.streaming::after {
content:"▍"; color:var(--accent); margin-left:2px;
animation:chat-blink 1s steps(2) infinite;
}
@keyframes chat-blink { 50% { opacity:0; } }
.chat-compose-wrap {
flex-shrink:0; padding:12px 16px 16px; border-top:1px solid var(--border);
background:var(--panel);
}
.chat-compose {
display:flex; gap:8px; align-items:flex-end; max-width:768px; margin:0 auto;
padding:10px 12px; border:1px solid var(--border); border-radius:16px;
background:var(--chat-input-bg);
}
.chat-compose:focus-within {
border-color:var(--accent);
box-shadow:0 0 0 1px var(--accent);
}
.chat-compose textarea {
flex:1; min-height:24px; max-height:200px; resize:none; width:auto;
border:0; background:transparent; padding:4px 0; outline:none;
color:var(--fg); caret-color:var(--accent); font:inherit; font-size:14px; line-height:1.5;
}
.chat-compose textarea::placeholder { color:var(--dim); opacity:1; }
.chat-compose button {
flex-shrink:0; min-width:36px; height:36px; padding:0;
border-radius:8px; border:1px solid var(--chat-user-border);
background:var(--chat-user-bg); color:#f0f6fc;
}
.chat-compose button:hover:not(:disabled) {
border-color:var(--accent); background:var(--chat-user-border);
}
.chat-compose button:disabled { opacity:.45; cursor:not-allowed; }
.console {
background:var(--bg); border:1px solid var(--border); border-radius:6px;
min-height:160px; max-height:280px; overflow:auto; padding:7px 9px;
white-space:pre-wrap; word-break:break-word; font-size:11px;
}
.console-line { padding:1px 0; border-bottom:1px solid #161b22; }
.console-time { color:var(--dim); }
.console-level-info { color:var(--accent); }
.console-level-warn { color:var(--warn); }
.console-level-error { color:var(--bad); }
.testing-controls { display:flex; gap:8px; margin-bottom:8px; }
.testing-controls input { flex:1; background:var(--bg); border:1px solid var(--border);
color:var(--fg); border-radius:6px; padding:4px 8px; font-size:12px; }
.testing-error { border:1px solid var(--bad); border-radius:6px; color:var(--bad);
padding:6px 9px; margin-bottom:8px; font-size:12px; }
.testing-list { max-height:320px; overflow:auto; }
.testing-row { display:flex; align-items:center; gap:8px; padding:3px 0;
border-bottom:1px solid var(--border); font-size:12px; }
.testing-row .testing-target { flex:1; word-break:break-all; font-family:monospace; }
.testing-row button[disabled] { opacity:.45; cursor:not-allowed; }
.status-pending { color:var(--warn); }
.status-processing { color:var(--accent); }
.status-failed { color:var(--bad); }
.status-complete { color:var(--ok); }
.status-canceled { color:var(--dim); }
button.btn-cancel { color:var(--dim); padding:0 5px; min-width:1.4em; line-height:1.2; }
button.btn-cancel:hover { color:var(--bad); border-color:var(--bad); }
</style>
</head>
<body>
<header>
<h1>meshnet tracker</h1>
<span class="meta" id="self-url"></span>
<span class="meta" id="refreshed"></span>
<button class="small" id="refresh-btn" type="button" onclick="refreshActiveTab(true)" style="margin-left:auto">refresh</button>
</header>
<nav class="dashboard-tabs" aria-label="Dashboard sections">
<button id="tab-overview" class="active" onclick="switchDashboardTab('overview')">Overview</button>
<button id="tab-chat" onclick="switchDashboardTab('chat')">Chat</button>
<button id="tab-billing" style="display:none" onclick="switchDashboardTab('billing')">Billing</button>
<button id="tab-admin" style="display:none" onclick="switchDashboardTab('admin')">Admin</button>
<button id="tab-testing" style="display:none" onclick="switchDashboardTab('testing')">Testing</button>
</nav>
<main>
<section data-tab="overview" id="account-section"><h2>Account</h2><div id="account">loading…</div></section>
<section data-tab="overview"><h2>Tracker hive</h2><div id="hive" class="empty">loading…</div></section>
<section data-tab="overview"><h2>Nodes &amp; coverage</h2><div id="nodes" class="empty">loading…</div></section>
<section data-tab="overview"><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
<section data-tab="overview" class="wide"><h2>Routing (learned)</h2><div id="routing" class="empty">loading…</div></section>
<section data-tab="overview" class="wide"><h2>Model inference speed</h2><div id="model-speed-chart" class="empty">loading…</div></section>
<section data-tab="overview" class="wide"><h2>Call wall</h2><div id="call-wall" class="empty">loading...</div></section>
<section data-tab="chat" class="wide chat-section">
<h2 style="display:none">Chat / inference</h2>
<div class="chat-app">
<aside class="chat-sidebar">
<button type="button" class="chat-new-btn" onclick="createNewChatSession()">+ New chat</button>
<div id="chat-session-list" class="chat-session-list empty-state">No chats yet</div>
</aside>
<div class="chat-main">
<div class="chat-toolbar">
<label>Model
<select id="chat-model" onchange="selectChatModel(this.value)"></select>
<select id="chat-quantization" onchange="selectChatQuantization(this.value)"></select>
<button type="button" id="vote-coverage" onclick="voteForCoverage()" style="display:none">Vote for coverage</button>
</label>
<button type="button" id="request-model-load" style="display:none" onclick="requestSelectedModelLoad()">Load on available node</button>
<div id="chat-status" class="chat-status">select a model to start</div>
</div>
<div id="chat-history" class="chat-messages empty">
<div class="chat-messages-inner">Send a message to start this conversation.</div>
</div>
<div class="chat-compose-wrap">
<div class="chat-compose">
<textarea id="chat-prompt" placeholder="Message…" rows="1" aria-label="Message"></textarea>
<button type="button" onclick="onChatSendClick()" id="chat-send" title="Send (Enter)"></button>
</div>
</div>
</div>
</div>
</section>
<section data-tab="billing" data-logged-in-only><h2>Usage summary</h2><div id="usage-summary" class="empty">login required</div></section>
<section data-tab="billing" data-logged-in-only><h2>Node throughput</h2><div id="node-throughput" class="empty">login required</div></section>
<section data-tab="billing"><h2>Request history</h2><div id="billing-usage" class="empty">login required</div></section>
<section data-tab="billing" data-admin-only><h2>Node pending payouts</h2><div id="pending" class="empty">admin login required</div></section>
<section data-tab="billing" data-admin-only><h2>Settlement history</h2><div id="settlements" class="empty">admin login required</div></section>
<section data-tab="admin" id="admin-section"><h2>All accounts (admin)</h2><div id="admin" class="empty"></div></section>
<section data-tab="admin" data-admin-only><h2>Strikes / bans / forfeitures</h2><div id="fraud" class="empty">admin login required</div></section>
<section data-tab="admin"><h2>Client balances</h2><div id="clients" class="empty">admin login required</div></section>
<section data-tab="admin" class="wide"><h2>Console output</h2><div id="console" class="console empty">admin login required</div></section>
<section data-tab="testing" data-admin-only>
<h2>Test run status</h2>
<div id="testing-error" class="testing-error" hidden></div>
<div id="testing-status" class="empty">no test run yet</div>
</section>
<section data-tab="testing" data-admin-only>
<h2>Tests &amp; suites</h2>
<div class="testing-controls">
<input id="testing-filter" type="search" placeholder="filter tests…" oninput="renderTestTargets()" aria-label="Filter tests">
<select id="testing-tag-filter" onchange="renderTestTargets()" aria-label="filter by test tag">
<option value="">all tags</option>
</select>
<button class="small" type="button" data-test-target="suite:all">Run all tests</button>
<button class="small" type="button" onclick="reloadTestTargets()">re-collect</button>
</div>
<div id="testing-tag-actions" class="testing-tags"></div>
<div id="testing-targets" class="empty">admin login required</div>
</section>
<section data-tab="testing" data-admin-only class="wide">
<h2>Test output</h2>
<div id="testing-log" class="console empty">no test output yet</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 tps = v => (v === null || v === undefined) ? "?" : (Math.round(v * 10) / 10).toFixed(1);
const copies = v => (v === null || v === undefined) ? "?" : Number(v).toFixed(2);
const hp = v => (v === null || v === undefined) ? "?HP" : `${copies(v)}HP`;
const short = (s, n=14) => { s = String(s); return s.length > n ? s.slice(0, 6) + "…" + s.slice(-5) : s; };
function accountDisplayName(account) {
if (!account) return "?";
const nickname = (account.nickname || "").trim();
if (nickname) return nickname;
return account.email || account.wallet || account.account_id || "?";
}
function buildNodeNameMap(map) {
const byId = new Map();
for (const node of (map && map.nodes) || []) {
const name = (node.friendly_name || "").trim();
if (node.node_id && name) byId.set(node.node_id, name);
}
return byId;
}
function nodeDisplayName(nodeOrId, nameMap) {
if (nodeOrId && typeof nodeOrId === "object") {
const friendly = (nodeOrId.friendly_name || "").trim();
if (friendly) return friendly;
return short(nodeOrId.node_id || "?");
}
const friendly = nameMap && nameMap.get(nodeOrId);
return friendly || short(nodeOrId || "?");
}
function nodeDisplayCell(node) {
const label = esc(nodeDisplayName(node));
const friendly = (node.friendly_name || "").trim();
if (friendly && node.node_id) {
return `<span title="${esc(node.node_id)}">${label}</span>`;
}
return label;
}
function modelAliasKey(value) {
if (!value) return "";
const text = String(value).trim();
if (!text) return "";
const shortName = text.includes("/") ? text.split("/").pop() : text;
return shortName.toLowerCase();
}
function buildModelAliasMap(map) {
const byAlias = new Map();
const register = (display, ...names) => {
if (!display) return;
for (const name of names) {
const key = modelAliasKey(name);
if (key && !byAlias.has(key)) byAlias.set(key, display);
}
};
for (const entry of (map && map.recommended_models) || []) {
register(entry.id, entry.id, entry.hf_repo, ...(entry.aliases || []));
}
for (const entry of availableModels || []) {
register(entry.id, entry.id, entry.name, entry.hf_repo, ...(entry.aliases || []));
}
return byAlias;
}
function resolveModelGroup(node, aliasMap) {
for (const candidate of [node.hf_repo, node.model]) {
if (!candidate) continue;
const hit = aliasMap.get(modelAliasKey(candidate));
if (hit) return hit;
}
const raw = node.hf_repo || node.model || "?";
const key = modelAliasKey(raw);
return aliasMap.get(key) || key || "?";
}
function modelLookupKeys(model) {
const keys = new Set();
for (const value of [model.id, model.name, model.hf_repo, ...(model.aliases || [])]) {
if (!value) continue;
keys.add(String(value));
keys.add(modelAliasKey(value));
}
return keys;
}
function modelServedCopiesFromMap(map, model) {
const nodes = (map && map.nodes) || [];
if (!nodes.length) return model.servedCopies ?? 0;
const aliasMap = buildModelAliasMap(map);
const targetKeys = modelLookupKeys(model);
let best = model.servedCopies ?? 0;
for (const node of nodes) {
const nodeKeys = [resolveModelGroup(node, aliasMap), node.hf_repo, node.model]
.filter(Boolean)
.flatMap(value => [String(value), modelAliasKey(value)]);
if (!nodeKeys.some(key => targetKeys.has(key))) continue;
const served = node.model_supply && node.model_supply.served_model_copies;
if (served !== null && served !== undefined) best = Math.max(best, served);
}
return best;
}
const panelSig = {};
let pendingChatModelRefresh = false;
function stableSig(value) {
return JSON.stringify(value);
}
function renderIfChanged(panelId, data, renderFn) {
const sig = stableSig(data);
if (panelSig[panelId] === sig) return false;
panelSig[panelId] = sig;
renderFn(data);
return true;
}
function isEditing(idOrEl) {
const el = typeof idOrEl === "string" ? $(idOrEl) : idOrEl;
if (!el) return false;
const active = document.activeElement;
return active === el || (active && el.contains(active));
}
function refreshBlocked() {
return selectionActive()
|| chatBusy
|| isEditing("account-nickname")
|| isEditing("chat-prompt")
|| isEditing("chat-model");
}
function syncKeyedList(container, items, keyFn, sigFn, createFn, patchFn) {
if (!container) return;
const sig = stableSig(items.map(item => sigFn(item)));
if (container.dataset.listSig === sig) return;
container.dataset.listSig = sig;
if (!items.length) {
container.className = "chat-session-list empty-state";
container.replaceChildren(document.createTextNode("No chats yet"));
return;
}
container.className = "chat-session-list";
const existing = new Map();
for (const child of container.querySelectorAll("[data-session-id]")) {
existing.set(child.dataset.sessionId, child);
}
const seen = new Set();
for (const item of items) {
const key = keyFn(item);
seen.add(key);
let el = existing.get(key);
if (!el) {
el = createFn(item);
} else {
patchFn(el, item);
}
container.appendChild(el);
}
for (const [key, el] of existing) {
if (!seen.has(key)) el.remove();
}
}
function chatModelsSignature() {
return availableModels.map(model => ({
id: model.id,
label: chatModelOptionLabel(model, lastRouting),
}));
}
async function fetchJson(path) {
try {
const headers = {};
const token = localStorage.getItem("meshnet_session");
if (token) headers["Authorization"] = "Bearer " + token;
const r = await fetch(path, { headers, credentials: "same-origin" });
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 aliasMap = buildModelAliasMap(map);
const byModel = {};
for (const n of nodes) {
const key = resolveModelGroup(n, aliasMap);
(byModel[key] = byModel[key] || []).push(n);
}
const modelNames = Object.keys(byModel).sort((a, b) => a.localeCompare(b));
let html = "";
for (const model of modelNames) {
const group = byModel[model];
const servedValues = group
.map(n => n.model_supply && n.model_supply.served_model_copies)
.filter(v => v !== null && v !== undefined);
const served = servedValues.length ? Math.max(...servedValues) : undefined;
html += `<div><b>${esc(model)}</b> <span class="dim">(${group.length} node${group.length===1?"":"s"} · ${esc(copies(served))} served)</span></div>`;
html += table(["node", "shard", "tps (1h)", "queue", "served"], group.map(n => {
const modelStats = (n.throughput && (n.throughput[n.hf_repo] || n.throughput[n.model])) || {};
return [
nodeDisplayCell(n),
esc(`${n.shard_start ?? "?"}-${n.shard_end ?? "?"}`),
`<span class="num">${esc(tps(modelStats.tokens_per_sec_last_hour))}</span>`,
esc(String((n.stats && n.stats.queue_depth) ?? 0)),
`<span class="num">${esc(copies(n.model_supply && n.model_supply.served_model_copies))}</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);
}
function renderThroughputHtml(stats) {
const nodes = (stats && stats.nodes) || {};
const nameMap = buildNodeNameMap(lastNetworkMap);
const rows = [];
for (const [nodeId, nodeStats] of Object.entries(nodes)) {
for (const [model, s] of Object.entries((nodeStats && nodeStats.models) || {})) {
rows.push([
esc(nodeDisplayName(nodeId, nameMap)),
esc(short(model, 24)),
`<span class="num">${esc(tps(s.tokens_per_sec_last_hour))}</span>`,
`<span class="num">${esc(String(s.sample_count_last_hour ?? 0))}</span>`,
]);
}
}
if (!rows.length) return '<div class="empty">no throughput samples yet</div>';
return table(["node", "model", "tps (1h)", "samples"], rows);
}
function hiveThroughputSummary(stats) {
const nodes = (stats && stats.nodes) || {};
let totalTps = 0;
let samples = 0;
for (const nodeStats of Object.values(nodes)) {
for (const s of Object.values((nodeStats && nodeStats.models) || {})) {
const t = Number(s.tokens_per_sec_last_hour);
if (Number.isFinite(t)) totalTps += t;
samples += Number(s.sample_count_last_hour || 0);
}
}
return { totalTps, samples };
}
function enrichCallWallFromHeartbeat(states, map) {
const nodes = (map && map.nodes) || [];
for (const node of nodes) {
const reqs = (node.stats && node.stats.current_requests) || [];
for (const req of reqs) {
const id = req.request_id;
if (!id) continue;
let rec = states.get(id);
if (!rec) {
rec = {
id,
events: [],
status: "processing",
started: Date.now() / 1000 - Number(req.elapsed_seconds || 0),
model: req.model || node.model || "?",
};
states.set(id, rec);
} else if (rec.status === "pending") {
rec.status = "processing";
}
if (req.model) rec.model = req.model;
if (req.tokens != null) rec.tokens = req.tokens;
if (req.tokens_per_sec != null) rec.tps = req.tokens_per_sec;
if (req.elapsed_seconds != null) rec.elapsed = req.elapsed_seconds;
}
}
}
function buildCallWallStates(events) {
const byId = new Map();
for (const e of events) {
const f = e.fields || {};
const id = f.request_id;
if (!id) continue;
let rec = byId.get(id);
if (!rec) {
rec = { id, events: [] };
byId.set(id, rec);
}
rec.events.push(e);
const msg = e.message;
if (msg === "proxy route selected") {
rec.status = "pending";
rec.started = e.ts;
rec.model = f.model || f.route_model || "?";
rec.route = f.route || f.nodes;
rec.nodes = f.nodes;
rec.stream = f.stream;
} else if (msg === "proxy via relay" || msg === "proxy connected" || msg === "proxy connecting") {
rec.status = "processing";
if (!rec.started) rec.started = e.ts;
rec.model = rec.model || f.model || f.route_model || "?";
} else if (msg === "proxy progress") {
rec.status = "processing";
rec.model = rec.model || f.model || f.route_model || "?";
rec.tokens = f.tokens;
rec.tps = f.tokens_per_sec;
rec.elapsed = f.elapsed_seconds;
rec.stream = f.stream;
} else if (msg === "relay proxy failed, trying direct") {
rec.status = "processing";
rec.warn = "relay failed, trying direct";
} else if (msg === "proxy complete") {
rec.status = "complete";
rec.model = rec.model || f.model || f.route_model || "?";
rec.tokens = f.tokens;
rec.tps = f.tokens_per_sec;
rec.elapsed = f.elapsed_seconds;
rec.stream = f.stream;
rec.terminal = e;
} else if (msg === "proxy canceled") {
rec.status = "canceled";
rec.model = rec.model || f.model || f.route_model || "?";
rec.tokens = f.tokens;
rec.tps = f.tokens_per_sec;
rec.elapsed = f.elapsed_seconds;
rec.terminal = e;
} else if (msg === "proxy failed" || msg === "direct proxy failed after relay") {
rec.status = "failed";
rec.model = rec.model || f.model || f.route_model || "?";
rec.error = f.error || msg;
rec.terminal = e;
}
}
return byId;
}
function callWallAgeSeconds(rec, nowSec) {
const start = rec.started || (rec.events[0] && rec.events[0].ts) || nowSec;
return Math.max(0, nowSec - start);
}
function callWallMaxQueue(rec) {
const nodes = rec.nodes || [];
const nodeQueues = Array.isArray(nodes) ? nodes.map(n => Number(n.queue_depth || 0)) : [];
return nodeQueues.length ? Math.max(...nodeQueues) : 0;
}
function renderRouting(routing) {
const el = $("routing");
if (!el) return;
const models = (routing && routing.models) || {};
const entries = Object.entries(models);
if (!entries.length) {
el.innerHTML = '<div class="empty">no routable models yet</div>';
return;
}
const cfg = (routing && routing.config) || {};
const nameMap = buildNodeNameMap(lastNetworkMap);
let html = `<div class="dim" style="margin-bottom:6px">` +
`explore share: <b>${esc(String(cfg.explore_share ?? "?"))}</b> · ` +
`traffic ∝ tps^<b>${esc(String(cfg.weight_alpha ?? "?"))}</b> · ` +
`half-life: <b>${esc(String(cfg.stats_half_life_seconds ?? "?"))}s</b></div>`;
for (const [model, info] of entries) {
const routes = info.routes || [];
html += `<div style="margin-top:6px"><b>${esc(model)}</b> ` +
`<span class="dim">(epoch ${esc(String(info.epoch ?? 0))} · ${routes.length} route${routes.length === 1 ? "" : "s"})</span></div>`;
html += table(["route", "tps", "coeff", "share", "samples", "status"], routes.map(r => {
const hops = (r.hops || []).map(h => `${nodeDisplayName(h.node_id, nameMap)}[${h.shard}]`).join(" → ");
const statusCls = r.status === "proven" ? "ok" : r.status === "stale" ? "warn" : "dim";
const coeff = (r.coefficient === null || r.coefficient === undefined)
? "—" : Number(r.coefficient).toFixed(2) + "×";
return [
esc(hops || short(r.signature, 40)),
`<span class="num">${esc(r.tps === null || r.tps === undefined ? "—" : tps(r.tps))}</span>`,
`<span class="num">${esc(coeff)}</span>`,
`<span class="num">${esc(Math.round((r.expected_share || 0) * 100) + "%")}</span>`,
`<span class="num">${esc(String(r.samples ?? 0))}</span>`,
`<span class="${statusCls}">${esc(r.status || "?")}</span>`,
];
}));
}
el.innerHTML = html;
}
function renderModelSpeed(reports) {
const el = $("model-speed-chart");
if (!el) return;
const models = Object.entries(reports || {});
if (!models.length) { el.innerHTML = '<div class="empty">no observed route samples yet</div>'; return; }
let html = '';
for (const [model, report] of models) {
const routes = report.routes || [];
html += `<div style="margin:8px 0"><b>${esc(model)}</b>`;
html += table(["hops", "devices", "latency", "hop penalty", "speed", "samples", "route drill"], routes.map(route => [
`<span class="num">${esc(String(route.hop_count))}</span>`, esc(route.device_mix || "?"),
`<span class="num">${esc(route.latency_ms == null ? "—" : route.latency_ms.toFixed(2) + " ms")}</span>`,
`<span class="num">${esc(route.latency_penalty_ms == null ? "—" : route.latency_penalty_ms.toFixed(2) + " ms")}</span>`,
`<span class="num">${esc(tps(route.tps))}</span>`, `<span class="num">${esc(String(route.samples || 0))}</span>`,
`<details><summary>nodes</summary><span class="dim">${esc((route.node_ids || []).join(" → "))}</span></details>`,
]));
html += '</div>';
}
el.innerHTML = html;
}
function renderCallWall(consoleData, stats, map) {
const events = (consoleData && consoleData.events) || [];
const nowSec = Date.now() / 1000;
const states = buildCallWallStates(events);
enrichCallWallFromHeartbeat(states, map);
const active = [];
const terminal = [];
for (const rec of states.values()) {
if (rec.status === "pending" || rec.status === "processing") active.push(rec);
else if (rec.status === "complete" || rec.status === "failed" || rec.status === "canceled") terminal.push(rec);
}
active.sort((a, b) => (a.started || 0) - (b.started || 0));
terminal.sort((a, b) => (b.terminal && b.terminal.ts) - (a.terminal && a.terminal.ts));
const hive = hiveThroughputSummary(stats);
const pending = active.filter(r => r.status === "pending").length;
const processing = active.filter(r => r.status === "processing").length;
const failedRecent = terminal.filter(r => r.status === "failed").length;
let queuedEstimate = 0;
for (const rec of active) queuedEstimate += Math.max(0, callWallMaxQueue(rec) - 1);
let html =
`<div class="dim" style="margin-bottom:6px">` +
`hive tps (1h): <b>${esc(tps(hive.totalTps))}</b> · samples: <b>${hive.samples}</b> · ` +
`active: <span class="status-processing">${processing}</span> processing · ` +
`<span class="status-pending">${pending}</span> pending` +
(queuedEstimate ? ` · queued estimate: <b>${queuedEstimate}</b>` : "") +
(failedRecent ? ` · <span class="status-failed">${failedRecent} recent failures</span>` : "") +
`</div>`;
if (active.length) {
const canCancelProxies = isAdmin || !isLoggedIn;
const headers = ["status", "age", "model", "request", "live tps", "tokens", "queue", "route / note"];
if (canCancelProxies) headers.push("");
html += table(headers, active.map(rec => {
const statusCls = rec.status === "pending" ? "status-pending" : "status-processing";
const note = rec.warn || (rec.route ? short(String(rec.route), 28) : "");
const row = [
`<span class="${statusCls}">${esc(rec.status)}</span>`,
`<span class="num">${esc(rec.elapsed != null ? Number(rec.elapsed).toFixed(1) : callWallAgeSeconds(rec, nowSec).toFixed(1))}s</span>`,
esc(short(rec.model || "?", 28)),
esc(short(rec.id, 18)),
`<span class="num">${esc(tps(rec.tps))}</span>`,
`<span class="num">${esc(String(rec.tokens ?? "—"))}</span>`,
`<span class="num">${esc(String(callWallMaxQueue(rec)))}</span>`,
esc(note),
];
if (canCancelProxies) {
row.push(
`<button type="button" class="small btn-cancel" data-cancel-request="${esc(rec.id)}" title="Cancel request">×</button>`,
);
}
return row;
}));
} else {
html += '<div class="empty">no in-flight requests</div>';
}
const historyRows = terminal.slice(0, 40).map(rec => {
const e = rec.terminal || {};
const f = e.fields || {};
const statusCls = rec.status === "failed"
? "status-failed"
: rec.status === "canceled"
? "status-canceled"
: "status-complete";
const detail = rec.status === "failed"
? esc(short(rec.error || "?", 40))
: rec.status === "canceled"
? "canceled"
: (f.stream ? "stream" : "json");
return [
new Date((e.ts || 0) * 1000).toLocaleTimeString(),
`<span class="${statusCls}">${esc(rec.status)}</span>`,
esc(short(rec.model || "?", 28)),
esc(short(rec.id, 18)),
`<span class="num">${esc(tps(rec.tps ?? f.tokens_per_sec))}</span>`,
`<span class="num">${esc(String(rec.tokens ?? f.tokens ?? "?"))}</span>`,
`<span class="num">${esc(String(rec.elapsed ?? f.elapsed_seconds ?? "?"))}</span>`,
detail,
];
});
html += '<div style="margin-top:8px"><b class="dim">recent completed / failed / canceled</b></div>';
html += historyRows.length
? table(["time", "status", "model", "request", "tps", "tokens", "sec", "detail"], historyRows)
: '<div class="empty">no completed requests yet</div>';
$("call-wall").innerHTML = html;
}
function startOfLocalDay(tsSec) {
const d = new Date(tsSec * 1000);
d.setHours(0, 0, 0, 0);
return d.getTime() / 1000;
}
function formatUsageDayLabel(tsSec) {
return new Date(tsSec * 1000).toLocaleDateString();
}
function summarizeUsageBuckets(records) {
const now = Date.now() / 1000;
const todayStart = startOfLocalDay(now);
const daySec = 86400;
const empty = () => ({ requests: 0, tokens: 0, cost: 0 });
const daily = [0, 1, 2].map(offset => ({
label: offset === 0 ? "Today" : offset === 1 ? "Yesterday" : formatUsageDayLabel(todayStart - offset * daySec),
...empty(),
}));
const last7 = { label: "Last 7 days", ...empty() };
const last30 = { label: "Last 30 days", ...empty() };
const total = { label: "All time", ...empty() };
for (const u of records) {
const ts = Number(u.ts || 0);
const tokens = Number(u.total_tokens || 0);
const cost = Number(u.cost || 0);
total.requests += 1;
total.tokens += tokens;
total.cost += cost;
if (ts >= now - 30 * daySec) {
last30.requests += 1;
last30.tokens += tokens;
last30.cost += cost;
}
if (ts >= now - 7 * daySec) {
last7.requests += 1;
last7.tokens += tokens;
last7.cost += cost;
}
for (let offset = 0; offset < 3; offset++) {
const start = todayStart - offset * daySec;
const end = start + daySec;
if (ts >= start && ts < end) {
daily[offset].requests += 1;
daily[offset].tokens += tokens;
daily[offset].cost += cost;
}
}
}
return [...daily, last7, last30, total];
}
function renderUsageSummary(records) {
const el = $("usage-summary");
if (!el) return;
if (!sessionToken) {
el.innerHTML = '<div class="empty">login required</div>';
return;
}
if (!records.length) {
el.innerHTML = '<div class="empty">no billed requests yet</div>';
return;
}
const rows = summarizeUsageBuckets(records).map(b => [
esc(b.label),
`<span class="num">${b.requests}</span>`,
`<span class="num">${esc(String(b.tokens))}</span>`,
`<span class="num">${usdt(b.cost)}</span>`,
]);
el.innerHTML =
'<div class="dim" style="margin-bottom:6px">per-request detail on Request history below</div>' +
table(["period", "requests", "tokens", "cost (USDT)"], rows);
}
function renderNodeThroughput(stats) {
const el = $("node-throughput");
if (!el) return;
if (!sessionToken) {
el.innerHTML = '<div class="empty">login required</div>';
return;
}
el.innerHTML = renderThroughputHtml(stats);
}
function renderBillingUsage(records) {
const el = $("billing-usage");
if (!el) return;
if (!sessionToken) {
el.innerHTML = '<div class="empty">login required</div>';
return;
}
if (!records.length) {
el.innerHTML = '<div class="empty">no billed requests yet</div>';
return;
}
const rows = records.slice().reverse().map(u => [
new Date((u.ts || 0) * 1000).toLocaleString(),
esc(short(u.model || "?", 28)),
esc(short(u.api_key || "?", 14)),
`<span class="num">${esc(String(u.total_tokens))}</span>`,
`<span class="num">${usdt(u.cost)}</span>`,
]);
el.innerHTML = `<div class="dim" style="margin-bottom:6px">${records.length} request${records.length === 1 ? "" : "s"}</div>` +
table(["time", "model", "api key", "tokens", "cost (USDT)"], rows);
}
function renderConsole(data) {
const events = (data && data.events) || [];
if (!events.length) {
$("console").innerHTML = '<div class="empty">no console events</div>';
return;
}
$("console").innerHTML = events.slice(-120).map(e => {
const level = String(e.level || "info");
const cls = level === "error" ? "console-level-error" : level === "warn" ? "console-level-warn" : "console-level-info";
const fields = e.fields && Object.keys(e.fields).length ? " " + JSON.stringify(e.fields) : "";
return `<div class="console-line"><span class="console-time">${new Date((e.ts || 0) * 1000).toLocaleTimeString()}</span> ` +
`<span class="${cls}">${esc(level.toUpperCase())}</span> ${esc(e.message || "")}${esc(fields)}</div>`;
}).join("");
}
// ---- testing tab (opt-in tracker test runner, dashboard-test-runner US-002) ----
let testCollection = { tests: [], test_metadata: [], suites: [], tags: [] };
let testRun = null;
let testRunStarting = false;
const TEST_RUN_POLL_MS = 1500;
const TEST_LOG_MAX_LINES = 400;
function testRunActive() {
return testRunStarting || (testRun !== null && testRun.status === "running");
}
function showTestingError(message) {
const el = $("testing-error");
if (!el) return;
el.hidden = !message;
el.textContent = message || "";
}
// Non-2xx from the tracker carries {"error": ...}; 403 also covers the
// disabled-by-default runner, so surface the server's own wording.
function testingErrorText(result) {
if (!result) return "test runner request failed";
if (result.status === 0) return "test runner unreachable";
return (result.data && result.data.error) || `test runner request failed (HTTP ${result.status})`;
}
function fmtElapsed(seconds) {
if (typeof seconds !== "number" || !isFinite(seconds) || seconds < 0) return "—";
if (seconds < 60) return `${seconds.toFixed(1)}s`;
const mins = Math.floor(seconds / 60);
return `${mins}m ${Math.floor(seconds % 60)}s`;
}
function fmtTestTime(ts) {
if (typeof ts !== "number" || !ts) return "—";
return new Date(ts * 1000).toLocaleTimeString();
}
function testStatusClass(status) {
if (status === "running") return "status-processing";
if (status === "passed") return "status-complete";
if (status === "failed" || status === "timeout") return "status-failed";
return "status-pending";
}
function renderTestRunStatus() {
const el = $("testing-status");
if (!el) return;
if (!testRun) {
el.className = "empty";
el.innerHTML = testRunStarting ? "starting test run…" : "no test run yet";
return;
}
el.className = "";
const status = String(testRun.status || "unknown");
const exitCode = testRun.exit_code;
const outcome = status === "running"
? '<span class="status-processing">running</span>'
: status === "passed"
? '<span class="ok">success</span>'
: `<span class="bad">failure</span>`;
el.innerHTML = table(
["target", "state", "outcome", "started", "ended", "elapsed", "exit code"],
[[
`<span class="testing-target">${esc(testRun.target || "—")}</span>`,
`<span class="pill ${testStatusClass(status)}">${esc(status)}</span>`,
outcome,
fmtTestTime(testRun.started_at),
fmtTestTime(testRun.finished_at),
`<span class="num">${fmtElapsed(testRun.elapsed_seconds)}</span>`,
exitCode === null || exitCode === undefined
? '<span class="dim">—</span>'
: `<span class="num ${exitCode === 0 ? "ok" : "bad"}">${esc(exitCode)}</span>`,
]],
);
}
function renderTestLog(stdout, stderr) {
const el = $("testing-log");
if (!el) return;
const lines = [];
for (const line of String(stdout || "").split("\n")) {
if (line !== "") lines.push({ text: line, err: false });
}
for (const line of String(stderr || "").split("\n")) {
if (line !== "") lines.push({ text: line, err: true });
}
if (!lines.length) {
el.className = "console empty";
el.innerHTML = testRunActive() ? "waiting for test output…" : "no test output yet";
return;
}
// Bounded view: the tracker already caps retained lines; cap the DOM too.
const shown = lines.slice(-TEST_LOG_MAX_LINES);
const pinned = el.scrollTop + el.clientHeight >= el.scrollHeight - 24;
el.className = "console";
el.innerHTML = shown.map(line =>
`<div class="console-line${line.err ? " console-level-error" : ""}">${esc(line.text)}</div>`
).join("");
if (pinned) el.scrollTop = el.scrollHeight;
}
function renderTestTargets() {
const el = $("testing-targets");
if (!el) return;
const filter = ($("testing-filter")?.value || "").trim().toLowerCase();
const selectedTag = ($("testing-tag-filter")?.value || "").trim().toLowerCase();
const metadata = testCollection.test_metadata || testCollection.tests.map(id => ({ id, description: id, tags: [] }));
const suites = (testCollection.suites || [])
.map(s => ({ id: s.id, label: `${s.name} (${(s.paths || []).join(", ")})`, description: "Approved test suite", tags: ["suite"], suite: true }));
const tests = metadata.map(t => ({ ...t, label: t.id, suite: false }));
const targets = [...suites, ...tests]
.filter(t => !filter || `${t.label} ${t.description} ${(t.tags || []).join(" ")}`.toLowerCase().includes(filter))
.filter(t => !selectedTag || t.suite || (t.tags || []).includes(selectedTag));
const disabled = testRunActive() ? " disabled" : "";
const tagFilter = $("testing-tag-filter");
if (tagFilter) {
const value = tagFilter.value;
tagFilter.innerHTML = '<option value="">all tags</option>' +
(testCollection.tags || []).map(t => `<option value="${esc(t.name)}">${esc(t.name)} (${esc(t.count)})</option>`).join("");
tagFilter.value = value;
}
const tagActions = $("testing-tag-actions");
if (tagActions) {
tagActions.innerHTML = (testCollection.tags || []).map(t =>
`<button class="small" type="button" data-test-target="${esc(t.id)}"${disabled}>Run ${esc(t.name)} (${esc(t.count)})</button>`
).join("");
}
if (!targets.length) {
el.className = "empty";
el.innerHTML = filter || selectedTag ? "no tests match the filter" : "no tests collected";
return;
}
el.className = "testing-list";
el.innerHTML = targets.map(t =>
`<div class="testing-row">` +
`<span class="testing-target"><strong>${t.suite ? '<span class="pill">suite</span> ' : ""}${esc(t.label)}</strong>` +
`<br><span class="dim">${esc(t.description || "")}</span>` +
`${(t.tags || []).map(tag => ` <span class="pill">${esc(tag)}</span>`).join("")}</span>` +
`<button class="small" type="button" data-test-target="${esc(t.id)}"${disabled}>Run</button>` +
`</div>`
).join("");
}
function renderTesting() {
renderTestRunStatus();
renderTestTargets();
}
function applyTestStatus(result) {
if (!result || !result.ok) {
showTestingError(testingErrorText(result));
return false;
}
testRun = result.data.run || null;
renderTestRunStatus();
renderTestLog(result.data.stdout, result.data.stderr);
return true;
}
async function runTest(target) {
if (testRunActive()) return;
showTestingError("");
testRunStarting = true;
renderTesting();
const result = await apiCall("/v1/tests/run", "POST", { target });
testRunStarting = false;
if (!result.ok) {
showTestingError(testingErrorText(result));
renderTesting();
return;
}
testRun = result.data.run || null;
renderTesting();
renderTestLog(result.data.stdout, result.data.stderr);
}
async function loadTestTargets(refresh) {
const result = await apiCall(`/v1/tests${refresh ? "?refresh=1" : ""}`);
if (!result.ok) {
showTestingError(testingErrorText(result));
$("testing-targets").className = "empty";
$("testing-targets").innerHTML = "test targets unavailable";
return false;
}
testCollection = {
tests: result.data.tests || [],
test_metadata: result.data.test_metadata || [],
suites: result.data.suites || [],
tags: result.data.tags || [],
};
renderTestTargets();
return true;
}
async function reloadTestTargets() {
showTestingError("");
await loadTestTargets(true);
}
async function fetchTestingTab() {
if (!isAdmin) return;
showTestingError("");
const [, statusResult] = await Promise.all([
loadTestTargets(false),
apiCall("/v1/tests/status"),
]);
applyTestStatus(statusResult);
renderTestTargets();
}
// Auto-refresh status + log while a run is in flight (independent of the
// selection/edit guards that pause the other panels — logs must keep flowing).
async function pollTestRunIfActive() {
if (dashboardTab !== "testing" || !isAdmin || !testRunActive()) return;
applyTestStatus(await apiCall("/v1/tests/status"));
// Re-enable the Run buttons on the transition out of "running".
if (!testRunActive()) renderTestTargets();
}
function bindTestingControls() {
document.addEventListener("click", event => {
const button = event.target.closest("[data-test-target]");
if (!button || button.disabled) return;
event.preventDefault();
void runTest(button.getAttribute("data-test-target"));
});
}
// ---- account panel (registration / login / balance / usage / API keys) ----
let sessionToken = localStorage.getItem("meshnet_session") || null;
let authTab = "login";
let dashboardTab = "overview";
let isAdmin = false;
let isLoggedIn = false;
let accountApiKeys = [];
let accountUsageRecords = [];
let lastStats = null;
let lastRouting = null;
let lastNetworkMap = null;
let availableModels = [];
let chatHistory = [];
let chatBusy = false;
let chatSessions = [];
let activeChatSessionId = "";
let selectedChatModel = localStorage.getItem("meshnet_chat_model") || "";
let selectedChatQuantization = localStorage.getItem("meshnet_chat_quantization") || "";
const CHAT_SESSIONS_KEY = "meshnet_chat_sessions_v1";
const CHAT_ACTIVE_SESSION_KEY = "meshnet_chat_active_session_v1";
const CHAT_SESSIONS_LIMIT = 50;
function newChatSessionId() {
if (window.crypto && crypto.randomUUID) return crypto.randomUUID();
return "chat-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
}
function loadChatSessionsStore() {
try {
const raw = localStorage.getItem(CHAT_SESSIONS_KEY);
const parsed = raw ? JSON.parse(raw) : [];
if (!Array.isArray(parsed)) return [];
for (const session of parsed) {
for (const msg of session.messages || []) {
delete msg.streaming;
delete msg.streamStats;
}
}
return parsed;
} catch {
return [];
}
}
function saveChatSessionsStore() {
localStorage.setItem(CHAT_SESSIONS_KEY, JSON.stringify(chatSessions));
if (activeChatSessionId) {
localStorage.setItem(CHAT_ACTIVE_SESSION_KEY, activeChatSessionId);
}
}
function chatSessionTitle(session) {
const firstUser = (session.messages || []).find(msg => msg.role === "user");
if (!firstUser || !firstUser.content) return "New chat";
const text = String(firstUser.content).trim().replace(/\s+/g, " ");
return text.length > 42 ? text.slice(0, 42) + "…" : text;
}
function formatSessionTime(iso) {
if (!iso) return "";
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return "";
const now = new Date();
const sameDay = date.toDateString() === now.toDateString();
if (sameDay) return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
return date.toLocaleDateString([], { month: "short", day: "numeric" });
}
function getActiveChatSession() {
return chatSessions.find(session => session.id === activeChatSessionId) || null;
}
function persistActiveChatSession() {
const session = getActiveChatSession();
if (!session) return;
session.messages = chatHistory.slice();
session.model = selectedChatModel || session.model || "";
session.title = chatSessionTitle(session);
session.updatedAt = new Date().toISOString();
chatSessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
if (chatSessions.length > CHAT_SESSIONS_LIMIT) {
chatSessions = chatSessions.slice(0, CHAT_SESSIONS_LIMIT);
if (!chatSessions.some(item => item.id === activeChatSessionId)) {
activeChatSessionId = chatSessions[0].id;
chatHistory = chatSessions[0].messages.slice();
}
}
saveChatSessionsStore();
renderChatSessionList();
}
function clearChatPrompt() {
const promptEl = $("chat-prompt");
if (!promptEl) return;
promptEl.value = "";
promptEl.style.height = "auto";
}
function createNewChatSession() {
if (chatBusy) return;
const session = {
id: newChatSessionId(),
title: "New chat",
model: selectedChatModel || "",
messages: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
chatSessions.unshift(session);
activeChatSessionId = session.id;
chatHistory = [];
clearChatPrompt();
saveChatSessionsStore();
renderChatSessionList();
renderChatHistory(true);
renderChatAuthHint();
const promptEl = $("chat-prompt");
if (promptEl) promptEl.focus();
}
function selectChatSession(sessionId) {
if (chatBusy) return;
const session = chatSessions.find(item => item.id === sessionId);
if (!session) return;
if (sessionId === activeChatSessionId) return;
activeChatSessionId = session.id;
chatHistory = (session.messages || []).slice();
clearChatPrompt();
if (session.model) {
selectedChatModel = session.model;
localStorage.setItem("meshnet_chat_model", selectedChatModel);
const select = $("chat-model");
if (select) select.value = selectedChatModel;
}
localStorage.setItem(CHAT_ACTIVE_SESSION_KEY, activeChatSessionId);
renderChatSessionList();
renderChatHistory(true);
renderChatAuthHint();
}
function deleteChatSession(sessionId) {
if (chatBusy) return;
const index = chatSessions.findIndex(item => item.id === sessionId);
if (index < 0) return;
chatSessions.splice(index, 1);
if (activeChatSessionId === sessionId) {
if (chatSessions.length) {
activeChatSessionId = chatSessions[0].id;
chatHistory = (chatSessions[0].messages || []).slice();
clearChatPrompt();
if (chatSessions[0].model) {
selectedChatModel = chatSessions[0].model;
localStorage.setItem("meshnet_chat_model", selectedChatModel);
}
} else {
saveChatSessionsStore();
createNewChatSession();
return;
}
}
saveChatSessionsStore();
renderChatSessionList();
renderChatHistory(true);
renderChatModels();
}
function initChatSessions() {
chatSessions = loadChatSessionsStore();
activeChatSessionId = localStorage.getItem(CHAT_ACTIVE_SESSION_KEY) || "";
const active = chatSessions.find(session => session.id === activeChatSessionId);
if (!active) {
if (chatSessions.length) {
activeChatSessionId = chatSessions[0].id;
chatHistory = (chatSessions[0].messages || []).slice();
if (chatSessions[0].model) selectedChatModel = chatSessions[0].model;
} else {
createNewChatSession();
return;
}
} else {
chatHistory = (active.messages || []).slice();
if (active.model) selectedChatModel = active.model;
}
renderChatSessionList();
renderChatHistory(true);
}
function renderChatSessionList() {
const list = $("chat-session-list");
syncKeyedList(
list,
chatSessions,
session => session.id,
session => `${session.id}|${session.updatedAt || ""}|${chatSessionTitle(session)}|${session.id === activeChatSessionId}`,
createSessionRowElement,
patchSessionRowElement,
);
}
function createSessionRowElement(session) {
const el = document.createElement("div");
el.className = "chat-session-item";
el.dataset.sessionId = session.id;
el.setAttribute("role", "button");
el.tabIndex = 0;
const title = document.createElement("div");
title.className = "chat-session-title";
const meta = document.createElement("div");
meta.className = "chat-session-meta";
const del = document.createElement("button");
del.type = "button";
del.className = "chat-session-delete";
del.title = "Delete chat";
del.dataset.deleteSession = session.id;
del.textContent = "×";
el.append(title, meta, del);
patchSessionRowElement(el, session);
return el;
}
function patchSessionRowElement(el, session) {
el.classList.toggle("active", session.id === activeChatSessionId);
el.querySelector(".chat-session-title").textContent = chatSessionTitle(session);
const when = formatSessionTime(session.updatedAt || session.createdAt);
const meta = el.querySelector(".chat-session-meta");
if (when) {
meta.textContent = when;
meta.hidden = false;
} else {
meta.textContent = "";
meta.hidden = true;
}
}
function bindChatSessionList() {
const list = $("chat-session-list");
if (!list || list.dataset.bound) return;
list.dataset.bound = "1";
list.addEventListener("click", event => {
const deleteBtn = event.target.closest("[data-delete-session]");
if (deleteBtn) {
event.preventDefault();
event.stopPropagation();
deleteChatSession(deleteBtn.getAttribute("data-delete-session"));
return;
}
const item = event.target.closest("[data-session-id]");
if (item) selectChatSession(item.getAttribute("data-session-id"));
});
list.addEventListener("keydown", event => {
if (event.key !== "Enter" && event.key !== " ") return;
const deleteBtn = event.target.closest("[data-delete-session]");
if (deleteBtn) return;
const item = event.target.closest("[data-session-id]");
if (!item) return;
event.preventDefault();
selectChatSession(item.getAttribute("data-session-id"));
});
}
function switchDashboardTab(name) {
if ((name === "admin" || name === "testing") && !isAdmin) name = "overview";
if (name === "billing" && !isLoggedIn) name = "overview";
dashboardTab = name;
document.body.classList.toggle("chat-tab-active", name === "chat");
updateSectionVisibility();
for (const tabName of ["overview", "chat", "billing", "admin", "testing"]) {
const button = $("tab-" + tabName);
if (button) button.classList.toggle("active", tabName === dashboardTab);
}
void refreshActiveTab(true).then(() => {
if (dashboardTab === "chat") $("chat-prompt")?.focus();
});
}
function updateSectionVisibility() {
for (const section of document.querySelectorAll("main section[data-tab]")) {
const onTab = section.dataset.tab === dashboardTab;
const adminOnly = section.hasAttribute("data-admin-only");
const loggedInOnly = section.hasAttribute("data-logged-in-only");
section.hidden = !onTab || (adminOnly && !isAdmin) || (loggedInOnly && !isLoggedIn);
}
}
function renderChatStatus(text) {
$("chat-status").textContent = text;
}
function renderChatHistory(force) {
const history = $("chat-history");
if (!history) return;
if (!force && chatHistory.some(msg => msg.streaming)) return;
if (!chatHistory.length) {
if (history.dataset.historySig === "empty") return;
history.dataset.historySig = "empty";
history.className = "chat-messages empty";
history.innerHTML = '<div class="chat-messages-inner">Send a message to start this conversation.</div>';
return;
}
const sig = stableSig(chatHistory.map(msg => ({
role: msg.role,
content: msg.content,
streaming: Boolean(msg.streaming),
})));
if (!force && history.dataset.historySig === sig) return;
history.dataset.historySig = sig;
history.className = "chat-messages";
const nearBottom = history.scrollHeight - history.scrollTop - history.clientHeight < 80;
const rows = chatHistory.map(msg => chatMessageRowHtml(msg)).join("");
history.innerHTML = `<div class="chat-messages-inner">${rows}</div>`;
if (nearBottom || force) history.scrollTop = history.scrollHeight;
}
function findRoutingForModel(model, routing) {
const models = (routing && routing.models) || {};
const keys = [model.id, model.name, model.hf_repo, ...(model.aliases || [])].filter(Boolean);
for (const key of keys) {
if (models[key]) return models[key];
const aliasKey = modelAliasKey(key);
for (const [routeKey, info] of Object.entries(models)) {
if (modelAliasKey(routeKey) === aliasKey) return info;
}
}
return null;
}
function chatModelHealthHp(servedCopies) {
return hp(servedCopies);
}
function chatModelTypicalTps(model, routing) {
const info = findRoutingForModel(model, routing);
const routes = (info && info.routes) || [];
if (!routes.length) return null;
const proven = routes.filter(r => r.status === "proven" && Number(r.tps) > 0);
if (proven.length) return Math.max(...proven.map(r => Number(r.tps)));
const priors = routes.map(r => Number(r.prior_tps ?? r.tps ?? 0)).filter(v => v > 0);
return priors.length ? Math.max(...priors) : null;
}
function chatModelOptionLabel(model, routing) {
const base = model.name && model.name !== model.id
? `${model.name} (${model.id})`
: model.id;
const health = chatModelHealthHp(model.servedCopies);
const speed = chatModelTypicalTps(model, routing);
const speedText = speed === null ? "?" : `${tps(speed)} tok/s`;
const suffix = model.recommended ? " · recommended" : "";
return `${base} · ${health} · ${speedText}${suffix}`;
}
function renderChatModels(force) {
const select = $("chat-model");
if (!select) return;
const sig = stableSig(chatModelsSignature());
if (!force && panelSig.chatModels === sig) return;
if (!force && isEditing(select)) {
pendingChatModelRefresh = true;
return;
}
pendingChatModelRefresh = false;
panelSig.chatModels = sig;
const models = availableModels.slice();
if (!models.length) {
select.innerHTML = '<option value="">no models available</option>';
select.disabled = true;
return;
}
select.disabled = false;
const preferred = models.find(m => m.id === selectedChatModel)
|| models[0];
selectedChatModel = preferred.id;
localStorage.setItem("meshnet_chat_model", selectedChatModel);
select.innerHTML = models.map(model => {
const label = chatModelOptionLabel(model, lastRouting);
return `<option value="${esc(model.id)}"${model.id === selectedChatModel ? " selected" : ""}>${esc(label)}</option>`;
}).join("");
select.value = selectedChatModel;
renderChatQuantizations();
}
function selectedChatModelEntry() {
return availableModels.find(model => model.id === selectedChatModel) || null;
}
function renderChatQuantizations() {
const select = $("chat-quantization");
const vote = $("vote-coverage");
const model = selectedChatModelEntry();
if (!select || !vote) return;
const variants = (model && model.quantizations) || [];
const selectable = variants.filter(variant => variant.selectable);
if (!selectable.length) {
select.innerHTML = '<option value="">no complete coverage</option>';
select.disabled = true;
vote.style.display = model ? "" : "none";
return;
}
const preferred = selectable.find(variant => variant.id === selectedChatQuantization)
|| selectable[0];
selectedChatQuantization = preferred.id;
localStorage.setItem("meshnet_chat_quantization", selectedChatQuantization);
select.innerHTML = selectable.map(variant =>
`<option value="${esc(variant.id)}">${esc(variant.id)}</option>`
).join("");
select.value = selectedChatQuantization;
select.disabled = false;
vote.style.display = variants.some(variant => !variant.selectable) ? "" : "none";
}
function bindChatModelSelect() {
const select = $("chat-model");
if (!select || select.dataset.bound) return;
select.dataset.bound = "1";
select.addEventListener("blur", () => {
if (pendingChatModelRefresh) renderChatModels(true);
});
}
function selectChatModel(value) {
selectedChatModel = value || "";
localStorage.setItem("meshnet_chat_model", selectedChatModel);
const session = getActiveChatSession();
if (session) {
session.model = selectedChatModel;
session.updatedAt = new Date().toISOString();
saveChatSessionsStore();
renderChatSessionList();
}
renderChatQuantizations();
}
function selectChatQuantization(value) {
selectedChatQuantization = value || "";
localStorage.setItem("meshnet_chat_quantization", selectedChatQuantization);
}
async function voteForCoverage() {
const model = selectedChatModelEntry();
if (!model) return;
const unavailable = (model.quantizations || []).find(variant => !variant.selectable);
if (!unavailable) return;
const result = await apiCall("/v1/models/vote", "POST", {
model: model.id,
quantization: unavailable.id,
});
renderChatStatus(result.ok ? `vote recorded for ${unavailable.id} coverage` : "coverage vote failed");
}
async function requestSelectedModelLoad() {
if (!selectedChatModel) return;
const button = $("request-model-load");
if (button) button.disabled = true;
const result = await apiCall("/v1/models/load", "POST", { model: selectedChatModel });
if (button) button.disabled = false;
if (!result.ok) {
alert(result.data.error || "model load request failed");
return;
}
const assignment = result.data.assignment || {};
$("chat-status").textContent = `load queued on ${short(assignment.node_id || "node")} for layers ${assignment.shard_start}-${assignment.shard_end}`;
}
function chatAuthToken() {
if (accountApiKeys.length) return accountApiKeys[0];
return null;
}
function setAdminMode(enabled) {
isAdmin = enabled;
$("tab-admin").style.display = enabled ? "" : "none";
$("tab-testing").style.display = enabled ? "" : "none";
$("request-model-load").style.display = enabled ? "" : "none";
if (!enabled && (dashboardTab === "admin" || dashboardTab === "testing")) {
switchDashboardTab("overview");
} else {
updateSectionVisibility();
}
}
function setLoggedInMode(enabled) {
isLoggedIn = enabled;
$("tab-billing").style.display = enabled ? "" : "none";
if (!enabled) {
accountUsageRecords = [];
renderBillingUsage([]);
renderUsageSummary([]);
renderNodeThroughput(null);
if (dashboardTab === "billing") switchDashboardTab("overview");
} else {
updateSectionVisibility();
}
}
async function apiCall(path, method, body, bearerToken) {
const headers = { "Content-Type": "application/json" };
const token = bearerToken === undefined ? sessionToken : bearerToken;
if (token) headers["Authorization"] = "Bearer " + token;
try {
const r = await fetch(path, {
method: method || "GET",
headers,
credentials: "same-origin",
body: body ? JSON.stringify(body) : undefined,
});
const data = await r.json().catch(() => ({}));
return { ok: r.ok, status: r.status, data };
} catch {
return { ok: false, status: 0, data: {} };
}
}
function setSession(token) {
sessionToken = token;
if (token) localStorage.setItem("meshnet_session", token);
else localStorage.removeItem("meshnet_session");
}
function renderAuthForms(errorMsg) {
const tab = (name, label) =>
`<a class="${authTab === name ? "active" : ""}" onclick="switchAuthTab('${name}')">${label}</a>`;
const identityFields =
'<input id="auth-email" type="email" placeholder="email (or leave blank)">' +
'<input id="auth-wallet" type="text" placeholder="wallet address (or leave blank)">' +
'<input id="auth-nickname" type="text" placeholder="nickname (optional)">';
const form = authTab === "login"
? '<input id="auth-identifier" type="text" placeholder="email or wallet address">' +
'<input id="auth-password" type="password" placeholder="password">' +
'<div class="form-row"><button onclick="doLogin()">Log in</button></div>'
: identityFields +
'<input id="auth-password" type="password" placeholder="password (min 8 chars)">' +
'<div class="form-row"><button onclick="doRegister()">Create account</button></div>';
$("account").innerHTML =
`<div class="tabs">${tab("login", "Log in")}${tab("register", "Register")}</div>` +
form + `<div class="error-msg">${errorMsg ? esc(errorMsg) : ""}</div>`;
accountApiKeys = [];
renderChatAuthHint();
setLoggedInMode(false);
setAdminMode(false);
}
function switchAuthTab(name) { authTab = name; renderAuthForms(); }
async function doRegister() {
const email = $("auth-email").value.trim();
const wallet = $("auth-wallet").value.trim();
const nickname = $("auth-nickname").value.trim();
const password = $("auth-password").value;
const r = await apiCall("/v1/auth/register", "POST",
{ email: email || null, wallet: wallet || null, nickname: nickname || null, password });
if (!r.ok) { renderAuthForms(r.data.error || "registration failed"); return; }
setSession(r.data.session_token);
await loadAccountSummary(true);
await refreshActiveTab(true);
}
async function doLogin() {
const identifier = $("auth-identifier").value.trim();
const password = $("auth-password").value;
const r = await apiCall("/v1/auth/login", "POST", { identifier, password });
if (!r.ok) { renderAuthForms(r.data.error || "login failed"); return; }
setSession(r.data.session_token);
await loadAccountSummary(true);
await refreshActiveTab(true);
}
async function doLogout() {
await apiCall("/v1/auth/logout", "POST", {});
setSession(null);
renderAuthForms();
}
async function createKey() {
const r = await apiCall("/v1/account/keys", "POST", {});
if (r.ok) await loadAccountSummary(true);
}
async function revokeKey(key) {
if (!confirm("Revoke this API key? Clients using it will stop working.")) return;
await apiCall("/v1/account/keys/revoke", "POST", { api_key: key });
await loadAccountSummary(true);
}
async function topupKey(key) {
const r = await apiCall("/v1/account/topup", "POST", { api_key: key });
if (!r.ok) alert(r.data.error || "top-up failed");
await loadAccountSummary(true);
}
const COPY_TOOLTIP_MS = 2000;
function showCopiedTooltip(anchor) {
const box = (anchor && anchor.closest && anchor.closest(".keybox")) || anchor;
if (!box) return;
const existing = box.querySelector(".copy-tooltip");
if (existing) existing.remove();
const tip = document.createElement("span");
tip.className = "copy-tooltip";
tip.textContent = "Copied!";
tip.setAttribute("role", "status");
box.appendChild(tip);
setTimeout(() => tip.remove(), COPY_TOOLTIP_MS);
}
async function copyApiKeyText(text, anchor) {
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch { /* ignore */ }
document.body.removeChild(ta);
}
if (anchor) showCopiedTooltip(anchor);
}
function selectApiKeyText(el) {
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
if (!sel) return;
sel.removeAllRanges();
sel.addRange(range);
}
function copyApiKeyFromTextEl(el) {
const key = el.dataset.key || el.textContent || "";
return copyApiKeyText(key, el);
}
function copyApiKeyFromButton(button) {
const el = button.closest(".keybox") && button.closest(".keybox").querySelector(".key-text");
const key = (el && el.dataset.key) || "";
return copyApiKeyText(key, button);
}
function renderChatAuthHint() {
if (chatAuthToken()) {
renderChatStatus("ready to send with your active API key");
} else if (sessionToken) {
renderChatStatus("create an API key in Account to use chat on a billing-enabled tracker");
} else {
renderChatStatus("log in if this tracker requires an API key");
}
}
let accountTopupAmount = 0;
function buildAccountPanelShell() {
$("account").innerHTML =
`<div id="account-panel-root">` +
`<div><b data-bind="who"></b> <span class="pill" data-bind="role"></span> ` +
`<button class="small" style="float:right" onclick="doLogout()">log out</button></div>` +
`<div style="margin:6px 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap">` +
`<label class="dim" style="margin:0">nickname</label>` +
`<input id="account-nickname" type="text" placeholder="display name" style="min-width:180px">` +
`<button class="small" type="button" onclick="saveNickname()">save</button></div>` +
`<div style="margin:6px 0">balance: <b data-bind="balance"></b> USDT · requests: <b data-bind="requests"></b> · ` +
`tokens: <b data-bind="tokens"></b> · spent: <b data-bind="spent"></b> USDT</div>` +
`<div style="margin:6px 0"><b class="dim">API keys</b> ` +
`<button class="small" onclick="createKey()">+ new key</button></div>` +
`<div id="account-keys"></div></div>`;
}
function renderAccountKeys(api_keys, balances, topup_amount) {
const el = $("account-keys");
if (!el) return;
if (!api_keys.length) {
el.innerHTML = '<div class="empty">no active keys</div>';
return;
}
let html = "";
for (const key of api_keys) {
html += `<div class="keybox">` +
`<span class="key-text" data-key="${esc(key)}" onclick="selectApiKeyText(this)" ondblclick="copyApiKeyFromTextEl(this)">${esc(key)}</span>` +
`<span class="dim">(${usdt(balances[key] ?? 0)} USDT)</span>` +
`<button class="small" type="button" onclick="copyApiKeyFromButton(this)">copy</button>` +
(topup_amount > 0
? ` <button class="small" onclick="topupKey('${esc(key)}')">+${usdt(topup_amount)} (devnet)</button>`
: "") +
` <button class="small" onclick="revokeKey('${esc(key)}')">revoke</button></div>`;
}
el.innerHTML = html;
}
function patchAccountPanelView(account, api_keys, balances, total_balance, usage, topup_amount) {
const root = $("account-panel-root");
if (!root) return false;
accountTopupAmount = topup_amount || 0;
root.querySelector('[data-bind="who"]').textContent = accountDisplayName(account);
root.querySelector('[data-bind="role"]').textContent = account.role;
const bal = root.querySelector('[data-bind="balance"]');
bal.textContent = usdt(total_balance);
bal.className = total_balance > 0 ? "ok" : "bad";
root.querySelector('[data-bind="requests"]').textContent = String(usage.requests);
root.querySelector('[data-bind="tokens"]').textContent = String(usage.total_tokens);
root.querySelector('[data-bind="spent"]').textContent = usdt(usage.total_cost);
const nick = $("account-nickname");
if (nick && !isEditing(nick)) nick.value = account.nickname || "";
renderIfChanged("account-keys", { api_keys, balances, topup_amount }, data => {
renderAccountKeys(data.api_keys, data.balances, data.topup_amount);
});
return true;
}
async function saveNickname() {
const nickname = $("account-nickname").value.trim();
const r = await apiCall("/v1/account/profile", "POST", { nickname: nickname || null });
if (!r.ok) alert(r.data.error || "nickname update failed");
else await loadAccountSummary(true);
}
function applyAccountSummary(data, force) {
const { account, api_keys, balances, total_balance, usage, topup_amount } = data;
accountApiKeys = Array.isArray(api_keys) ? api_keys.slice() : [];
if (force || !patchAccountPanelView(account, api_keys, balances, total_balance, usage, topup_amount)) {
buildAccountPanelShell();
patchAccountPanelView(account, api_keys, balances, total_balance, usage, topup_amount);
}
renderChatAuthHint();
setLoggedInMode(true);
setAdminMode(account.role === "admin");
}
async function loadAccountSummary(force) {
const r = await apiCall("/v1/account");
if (r.status === 404) {
$("account-section").style.display = "none";
accountApiKeys = [];
accountUsageRecords = [];
renderChatAuthHint();
setLoggedInMode(false);
setAdminMode(false);
return false;
}
if (!r.ok) { setSession(null); renderAuthForms(); return false; }
applyAccountSummary(r.data, force);
if (availableModels.length) renderChatModels(Boolean(force));
return true;
}
async function loadAccountUsage() {
const r = await apiCall("/v1/account/usage");
if (!r.ok) return;
accountUsageRecords = (r.data.records || r.data.recent) || [];
renderIfChanged("usage-summary", accountUsageRecords, () => renderUsageSummary(accountUsageRecords));
renderIfChanged("billing-usage", accountUsageRecords, () => renderBillingUsage(accountUsageRecords));
}
/** @deprecated use loadAccountSummary */
async function renderAccountPanel(force) {
return loadAccountSummary(force);
}
let chatAbortController = null;
function onChatSendClick() {
if (chatBusy && chatAbortController) { chatAbortController.abort(); return; }
sendChat();
}
function setChatSendMode(streaming) {
const btn = $("chat-send");
if (!btn) return;
btn.textContent = streaming ? "■" : "↑";
btn.title = streaming ? "Stop generating" : "Send (Enter)";
}
function newChatStreamStats() {
return { tokens: 0, requestStarted: Date.now(), genStarted: null };
}
function chatStreamStatsText(stats) {
if (!stats) return "";
if (!stats.tokens) {
const waitS = Math.max((Date.now() - stats.requestStarted) / 1000, 0);
return `prefill… ${waitS.toFixed(0)}s`;
}
const base = stats.genStarted ?? stats.requestStarted;
const secs = Math.max((Date.now() - base) / 1000, 0.001);
return `${tps(stats.tokens / secs)} tok/s · ${stats.tokens} tokens`;
}
function chatMessageRowHtml(msg) {
const role = msg.role === "user" ? "user" : msg.role === "assistant" ? "assistant" : "error";
const streaming = msg.streaming ? " streaming" : "";
const body = esc(msg.content);
if (msg.streaming) {
const stats = esc(chatStreamStatsText(msg.streamStats));
return `<div class="chat-row ${role}"><div class="chat-bubble-stack"><div class="chat-bubble ${role}${streaming}">${body}</div><div class="chat-stream-stats" aria-live="polite">${stats}</div></div></div>`;
}
return `<div class="chat-row ${role}"><div class="chat-bubble ${role}${streaming}">${body}</div></div>`;
}
function updateStreamingChatBubble(content, streamStats) {
const history = $("chat-history");
if (!history) return;
const bubbles = history.querySelectorAll(".chat-bubble.assistant.streaming");
const bubble = bubbles[bubbles.length - 1];
if (!bubble) { renderChatHistory(true); return; }
bubble.textContent = content;
const stack = bubble.closest(".chat-bubble-stack");
const statsEl = stack && stack.querySelector(".chat-stream-stats");
if (statsEl && streamStats) statsEl.textContent = chatStreamStatsText(streamStats);
const nearBottom = history.scrollHeight - history.scrollTop - history.clientHeight < 80;
if (nearBottom) history.scrollTop = history.scrollHeight;
}
function finalizeAssistantMessage(msg) {
delete msg.streaming;
delete msg.streamStats;
if (!chatHistory.includes(msg)) chatHistory.push(msg);
if (!msg.content) msg.content = "(empty response)";
renderChatHistory(true);
persistActiveChatSession();
}
function removeAssistantMessage(msg) {
const index = chatHistory.indexOf(msg);
if (index >= 0) {
chatHistory.splice(index, 1);
renderChatHistory(true);
}
}
async function sendChat() {
const promptEl = $("chat-prompt");
const prompt = promptEl.value.trim();
if (!prompt || chatBusy) return;
if (!selectedChatModel) {
renderChatStatus("select a model first");
return;
}
const bearerToken = chatAuthToken();
const body = {
model: selectedChatModel,
messages: [
...chatHistory
.filter(msg => msg.role === "user" || msg.role === "assistant")
.map(msg => ({ role: msg.role, content: msg.content })),
{ role: "user", content: prompt },
],
stream: true,
quantization: selectedChatQuantization || "bfloat16",
max_tokens: 15120,
};
chatBusy = true;
setChatSendMode(true);
promptEl.value = "";
promptEl.style.height = "auto";
chatHistory.push({ role: "user", content: prompt, model: selectedChatModel });
const streamStats = newChatStreamStats();
const assistantMessage = {
role: "assistant",
content: "",
model: selectedChatModel,
streaming: true,
streamStats,
};
chatHistory.push(assistantMessage);
renderChatHistory(true);
persistActiveChatSession();
renderChatStatus("sending request…");
let tokens = 0;
let usage = null;
chatAbortController = new AbortController();
try {
const headers = { "Content-Type": "application/json" };
if (bearerToken) headers["Authorization"] = "Bearer " + bearerToken;
const resp = await fetch("/v1/chat/completions", {
method: "POST",
headers,
credentials: "same-origin",
body: JSON.stringify(body),
signal: chatAbortController.signal,
});
if (!resp.ok) {
let error = "request failed";
try {
const data = await resp.json();
error = typeof data.error === "string" ? data.error : (data.error && data.error.message) || error;
} catch { /* keep default */ }
throw new Error(error);
}
const contentType = resp.headers.get("Content-Type") || "";
if (!resp.body || !contentType.includes("text/event-stream")) {
const data = await resp.json();
assistantMessage.content = (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) || "";
usage = data.usage || null;
tokens = (usage && usage.completion_tokens) || 0;
} else {
renderChatStatus("waiting for first token…");
updateStreamingChatBubble(assistantMessage.content, streamStats);
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffered = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buffered += decoder.decode(value, { stream: true });
let newline;
while ((newline = buffered.indexOf("\n")) >= 0) {
const line = buffered.slice(0, newline).replace(/\r$/, "");
buffered = buffered.slice(newline + 1);
if (!line.startsWith("data:")) continue;
const payload = line.slice(5).trim();
if (!payload || payload === "[DONE]") continue;
let chunk;
try { chunk = JSON.parse(payload); } catch { continue; }
if (chunk.error) {
throw new Error(typeof chunk.error === "string" ? chunk.error : chunk.error.message || "stream error");
}
if (chunk.usage) usage = chunk.usage;
const delta = chunk.choices && chunk.choices[0] && chunk.choices[0].delta;
const piece = delta && delta.content;
if (piece) {
if (!streamStats.genStarted) streamStats.genStarted = Date.now();
assistantMessage.content += piece;
tokens += 1;
streamStats.tokens = tokens;
const status = chatStreamStatsText(streamStats);
updateStreamingChatBubble(assistantMessage.content, streamStats);
renderChatStatus(`generating… ${status}`);
}
}
}
}
finalizeAssistantMessage(assistantMessage);
const doneTps = streamStats.genStarted && tokens
? ` · ${tps(tokens / Math.max((Date.now() - streamStats.genStarted) / 1000, 0.001))} tok/s`
: "";
renderChatStatus(usage
? `done: ${usage.total_tokens ?? "?"} tokens${doneTps}`
: `done: ${tokens} tokens${doneTps}`);
} catch (err) {
if (err && err.name === "AbortError") {
if (assistantMessage.content) {
finalizeAssistantMessage(assistantMessage);
renderChatStatus(`stopped after ${tokens} tokens`);
} else {
removeAssistantMessage(assistantMessage);
persistActiveChatSession();
renderChatStatus("stopped");
}
} else {
console.error("chat stream failed", err);
removeAssistantMessage(assistantMessage);
const error = (err && err.message) || "request failed";
chatHistory.push({ role: "error", content: error, model: selectedChatModel });
renderChatHistory(true);
persistActiveChatSession();
renderChatStatus(error);
}
} finally {
chatBusy = false;
chatAbortController = null;
setChatSendMode(false);
promptEl.focus();
}
}
function bindChatPromptShortcuts() {
const promptEl = $("chat-prompt");
if (!promptEl || promptEl.dataset.bound === "1") return;
promptEl.dataset.bound = "1";
promptEl.addEventListener("keydown", event => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendChat();
}
});
promptEl.addEventListener("input", () => {
promptEl.style.height = "auto";
promptEl.style.height = Math.min(promptEl.scrollHeight, 200) + "px";
});
promptEl.addEventListener("blur", () => {
if (pendingChatModelRefresh) renderChatModels(true);
});
}
async function cancelProxyRequest(requestId) {
const r = await apiCall(
`/v1/proxy/requests/${encodeURIComponent(requestId)}/cancel`,
"POST",
{},
);
if (r.ok) await refreshCallWall();
}
$("call-wall").addEventListener("click", (event) => {
const button = event.target.closest("[data-cancel-request]");
if (!button) return;
event.preventDefault();
const requestId = button.getAttribute("data-cancel-request");
if (requestId) cancelProxyRequest(requestId);
});
function applyAvailableModels(models, map) {
lastNetworkMap = map || lastNetworkMap;
availableModels = ((models && models.data) || []).map(model => {
const entry = {
id: model.id,
name: model.name || model.id,
hf_repo: model.hf_repo,
recommended: Boolean(model.recommended),
aliases: model.aliases || [],
coverage: model.shard_coverage_percentage,
servedCopies: model.served_model_copies,
quantizations: model.quantizations || [],
};
entry.servedCopies = modelServedCopiesFromMap(lastNetworkMap, entry);
return entry;
}).filter(model => model.id);
}
async function preloadChatModels(forceRender) {
const [map, models, routing] = await Promise.all([
fetchJson("/v1/network/map"),
fetchJson("/v1/models"),
fetchJson("/v1/routing"),
]);
if (routing) lastRouting = routing;
applyAvailableModels(models, map);
if (forceRender !== false) {
panelSig.chatModels = null;
renderChatModels(true);
}
return { map, models, routing };
}
function markRefreshed() {
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
}
async function fetchOverviewTab() {
const fetches = [
fetchJson("/v1/raft/status"),
fetchJson("/v1/network/map"),
fetchJson("/v1/stats"),
fetchJson("/v1/routing"),
fetchJson("/v1/console"),
];
if (isLoggedIn) fetches.push(apiCall("/v1/account"));
const results = await Promise.all(fetches);
const [raft, map, stats, routing, consoleData, accountResp] = results;
lastStats = stats;
lastRouting = routing;
lastNetworkMap = map;
if (accountResp && accountResp.ok) applyAccountSummary(accountResp.data, false);
renderIfChanged("hive", raft, renderHive);
renderIfChanged("nodes", map, renderNodes);
renderIfChanged("stats", stats, renderStats);
renderIfChanged("routing", routing, renderRouting);
const speedModels = Object.keys((routing && routing.models) || {});
const reports = Object.fromEntries(await Promise.all(speedModels.map(async model => [
model, await fetchJson("/v1/model-speed?model=" + encodeURIComponent(model)),
])));
renderIfChanged("model-speed", reports, renderModelSpeed);
renderIfChanged("call-wall", { consoleData, stats, map }, data => renderCallWall(data.consoleData, data.stats, data.map));
}
async function fetchChatTab() {
await preloadChatModels(true);
if (isLoggedIn) {
const accountResp = await apiCall("/v1/account");
if (accountResp && accountResp.ok) applyAccountSummary(accountResp.data, false);
}
}
async function fetchBillingTab() {
const [stats, , accountResp, adminData] = await Promise.all([
fetchJson("/v1/stats"),
loadAccountUsage(),
isLoggedIn ? apiCall("/v1/account") : Promise.resolve(null),
isAdmin ? Promise.all([
fetchJson("/v1/billing/summary"),
fetchJson("/v1/billing/settlements"),
]) : Promise.resolve(null),
]);
lastStats = stats;
if (accountResp && accountResp.ok) applyAccountSummary(accountResp.data, false);
renderIfChanged("node-throughput", stats, renderNodeThroughput);
if (adminData) {
const [summary, settlements] = adminData;
renderIfChanged("billing-summary", summary, renderBilling);
renderIfChanged("settlements", settlements, renderSettlements);
}
}
async function fetchAdminTab() {
const fetches = [
fetchJson("/v1/console"),
fetchJson("/v1/billing/summary"),
fetchJson("/v1/registry/wallets"),
];
if (isAdmin) fetches.push(apiCall("/v1/admin/accounts"));
const results = await Promise.all(fetches);
const [consoleData, summary, wallets, adminResp] = results;
renderIfChanged("console", consoleData, renderConsole);
renderIfChanged("billing-summary", summary, data => renderBilling(data));
renderIfChanged("fraud", { wallets, summary }, data => renderFraud(data.wallets, data.summary));
if (adminResp && adminResp.ok) {
renderIfChanged("admin", adminResp.data.accounts || [], accounts => {
const rows = accounts.map(a => {
const balance = Object.values(a.balances || {}).reduce((x, y) => x + y, 0);
return [
esc(short(accountDisplayName(a), 24)),
`<span class="pill">${esc(a.role)}</span>`,
String((a.api_keys || []).length),
`<span class="num ${balance > 0 ? "ok" : "bad"}">${usdt(balance)}</span>`,
new Date(a.created_ts * 1000).toLocaleDateString(),
];
});
$("admin").innerHTML = table(["account", "role", "keys", "balance (USDT)", "created"], rows);
});
}
}
async function refreshCallWall() {
const [consoleData, map] = await Promise.all([
fetchJson("/v1/console"),
fetchJson("/v1/network/map"),
]);
if (map) lastNetworkMap = map;
renderIfChanged("call-wall", { consoleData, stats: lastStats, map }, data =>
renderCallWall(data.consoleData, data.stats, data.map));
}
const TAB_FETCHERS = {
overview: fetchOverviewTab,
chat: fetchChatTab,
billing: fetchBillingTab,
admin: fetchAdminTab,
testing: fetchTestingTab,
};
async function refreshActiveTab(force) {
$("self-url").textContent = location.host;
if (refreshBlocked() && !force) return;
const fetcher = TAB_FETCHERS[dashboardTab];
if (fetcher) await fetcher();
markRefreshed();
}
const CALL_WALL_POLL_MS = 5000;
async function pollCallWallIfIdle() {
if (dashboardTab !== "overview" || refreshBlocked()) return;
await refreshCallWall();
}
function selectionActive() {
const sel = window.getSelection();
return sel && !sel.isCollapsed && sel.toString().length > 0;
}
bindChatSessionList();
bindChatModelSelect();
initChatSessions();
bindChatPromptShortcuts();
bindTestingControls();
(async () => {
if (sessionToken) await loadAccountSummary(true);
else renderAuthForms();
await Promise.all([
preloadChatModels(true),
refreshActiveTab(true),
]);
renderChatHistory(true);
renderChatAuthHint();
})();
setInterval(pollCallWallIfIdle, CALL_WALL_POLL_MS);
setInterval(pollTestRunIfActive, TEST_RUN_POLL_MS);
</script>
</body>
</html>