diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html
index dc055e8..12076cc 100644
--- a/packages/tracker/meshnet_tracker/dashboard.html
+++ b/packages/tracker/meshnet_tracker/dashboard.html
@@ -127,9 +127,9 @@
display:flex; align-items:center; gap:8px; color:var(--dim); margin:0;
}
.chat-toolbar select {
- min-width:220px; max-width:min(420px, 50vw);
+ 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;
+ border-radius:6px; padding:6px 8px; font-size:13px;
}
.chat-status { color:var(--dim); font-size:12px; margin-left:auto; }
.chat-messages {
@@ -831,6 +831,7 @@ let isLoggedIn = false;
let accountApiKeys = [];
let accountUsageRecords = [];
let lastStats = null;
+let lastRouting = null;
let availableModels = [];
let chatHistory = [];
let chatBusy = false;
@@ -956,11 +957,7 @@ function selectChatSession(sessionId) {
renderChatAuthHint();
}
-function deleteChatSession(sessionId, event) {
- if (event) {
- event.preventDefault();
- event.stopPropagation();
- }
+function deleteChatSession(sessionId) {
if (chatBusy) return;
const index = chatSessions.findIndex(item => item.id === sessionId);
if (index < 0) return;
@@ -1020,18 +1017,43 @@ function renderChatSessionList() {
const active = session.id === activeChatSessionId ? " active" : "";
const title = esc(chatSessionTitle(session));
const when = esc(formatSessionTime(session.updatedAt || session.createdAt));
- const id = JSON.stringify(session.id);
+ const sessionId = esc(session.id);
return `
` +
+ ` data-session-id="${sessionId}">` +
`
${title}
` +
(when ? `
${when}
` : "") +
`
` +
+ ` data-delete-session="${sessionId}">×` +
`
`;
}).join("");
}
+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" && !isAdmin) name = "overview";
if (name === "billing" && !isLoggedIn) name = "overview";
@@ -1079,6 +1101,48 @@ function renderChatHistory() {
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 chatModelHealthLabel(coverage) {
+ if (coverage === null || coverage === undefined) return "unknown";
+ const pct = Math.round(Number(coverage));
+ if (pct >= 100) return "healthy";
+ if (pct > 0) return `${pct}% cov`;
+ return "no coverage";
+}
+
+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 = chatModelHealthLabel(model.coverage);
+ 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() {
const select = $("chat-model");
if (!select) return;
@@ -1094,11 +1158,8 @@ function renderChatModels() {
selectedChatModel = preferred.id;
localStorage.setItem("meshnet_chat_model", selectedChatModel);
select.innerHTML = models.map(model => {
- const label = model.name && model.name !== model.id
- ? `${model.name} (${model.id})`
- : model.id;
- const suffix = model.recommended ? " [recommended]" : "";
- return ``;
+ const label = chatModelOptionLabel(model, lastRouting);
+ return ``;
}).join("");
select.value = selectedChatModel;
}
@@ -1575,12 +1636,14 @@ 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,
})).filter(model => model.id);
renderHive(raft);
renderNodes(map);
@@ -1610,6 +1673,7 @@ async function refreshIfIdle() {
}
refresh();
+bindChatSessionList();
initChatSessions();
bindChatPromptShortcuts();
renderAccountPanel();
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index a077842..affa31d 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -52,6 +52,40 @@ def test_dashboard_chat_uses_streaming_fetch():
assert 'console.error("chat stream failed", err)' in html
+def test_dashboard_chat_model_selector_shows_health_and_speed():
+ tracker = TrackerServer()
+ port = tracker.start()
+ try:
+ html = urllib.request.urlopen(
+ f"http://127.0.0.1:{port}/dashboard"
+ ).read().decode()
+ finally:
+ tracker.stop()
+
+ assert "chatModelHealthLabel" in html
+ assert "chatModelTypicalTps" in html
+ assert "chatModelOptionLabel" in html
+ assert "findRoutingForModel" in html
+ assert "tok/s" in html
+
+
+def test_dashboard_chat_sessions_use_delegated_handlers():
+ tracker = TrackerServer()
+ port = tracker.start()
+ try:
+ html = urllib.request.urlopen(
+ f"http://127.0.0.1:{port}/dashboard"
+ ).read().decode()
+ finally:
+ tracker.stop()
+
+ assert "bindChatSessionList" in html
+ assert "data-session-id=" in html
+ assert "data-delete-session=" in html
+ assert 'onclick="selectChatSession(' not in html
+ assert 'onclick="deleteChatSession(' not in html
+
+
def test_dashboard_served_by_follower():
"""A tracker that is not the leader (unreachable peers → never elected)
still serves the dashboard from its own replicated state."""