diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index bf82ea9..db7abc3 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -277,7 +277,7 @@ const esc = s => String(s).replace(/[&<>"]/g, 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" : `${Number(v).toFixed(1)}HP`; +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 modelAliasKey(value) { @@ -317,6 +317,33 @@ function resolveModelGroup(node, aliasMap) { 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; +} + async function fetchJson(path) { try { const headers = {}; @@ -833,6 +860,7 @@ let accountApiKeys = []; let accountUsageRecords = []; let lastStats = null; let lastRouting = null; +let lastNetworkMap = null; let availableModels = []; let chatHistory = []; let chatBusy = false; @@ -1634,15 +1662,20 @@ async function refresh() { const [summary, settlements, wallets] = adminData; lastStats = stats; lastRouting = routing; - availableModels = ((models && models.data) || []).map(model => ({ - 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, - })).filter(model => model.id); + lastNetworkMap = map; + 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, + }; + entry.servedCopies = modelServedCopiesFromMap(map, entry); + return entry; + }).filter(model => model.id); renderHive(raft); renderNodes(map); renderBilling(summary); diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index dd6e650..d082538 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -63,11 +63,12 @@ def test_dashboard_chat_model_selector_shows_health_and_speed(): tracker.stop() assert "chatModelHealthHp" in html + assert "modelServedCopiesFromMap" in html assert "chatModelTypicalTps" in html assert "chatModelOptionLabel" in html assert "findRoutingForModel" in html assert "tok/s" in html - assert "0.0HP" in html or "toFixed(1)}HP" in html + assert "toFixed(2)}HP" in html or '${copies(v)}HP' in html def test_dashboard_chat_sessions_use_delegated_handlers():