From 91e4bcf2c9fbcd6d5af1154aa86ee79badca60cc Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Wed, 8 Jul 2026 19:49:52 +0300 Subject: [PATCH 1/3] connections --- packages/tracker/meshnet_tracker/dashboard.html | 1 + packages/tracker/meshnet_tracker/server.py | 6 +++++- tests/test_dashboard.py | 1 + tests/test_tracker_routing.py | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index 6bcd175..2c15801 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -1316,6 +1316,7 @@ async function sendChat() { persistActiveChatSession(); renderChatStatus("done"); } catch (err) { + console.error("chat stream failed", err); assistantMessage.role = "error"; assistantMessage.content = err && err.message ? err.message : "request failed"; renderChatHistory(); diff --git a/packages/tracker/meshnet_tracker/server.py b/packages/tracker/meshnet_tracker/server.py index 291992b..1e53cb2 100644 --- a/packages/tracker/meshnet_tracker/server.py +++ b/packages/tracker/meshnet_tracker/server.py @@ -3312,6 +3312,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): self.send_response(200) self.send_header("Content-Type", "text/event-stream; charset=utf-8") self.send_header("Cache-Control", "no-cache") + self.send_header("X-Accel-Buffering", "no") self.end_headers() stream_usage: dict | None = None observed_stream_tokens = 0 @@ -3354,7 +3355,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): if usage is not None: stream_usage = usage except (BrokenPipeError, ConnectionResetError): - pass + client_gone = True if self._finalize_proxy_cancel( proxy_ctx=proxy_ctx, server=server, @@ -3392,6 +3393,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): route_model=route_model, status=200, stream=True, + client_disconnected=client_gone, tokens=tokens, elapsed_seconds=round(elapsed, 4), tokens_per_sec=round(tokens / elapsed, 4) if elapsed > 0 else 0.0, @@ -3584,6 +3586,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): self.send_response(int(first.get("status", 200))) self.send_header("Content-Type", headers.get("Content-Type", "text/event-stream; charset=utf-8")) self.send_header("Cache-Control", "no-cache") + self.send_header("X-Accel-Buffering", "no") self.end_headers() server: _TrackerHTTPServer = self.server # type: ignore[assignment] stream_usage: dict | None = None @@ -3657,6 +3660,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): route_model=route_model, status=200, stream=True, + client_disconnected=client_gone, relay=True, tokens=tokens, elapsed_seconds=round(elapsed, 4), diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index ad05d48..af8fb5b 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -46,6 +46,7 @@ def test_dashboard_chat_uses_streaming_fetch(): assert "stream: true" in html assert ".body.getReader()" in html assert 'data === "[DONE]"' in html + assert 'console.error("chat stream failed", err)' in html def test_dashboard_served_by_follower(): diff --git a/tests/test_tracker_routing.py b/tests/test_tracker_routing.py index a6e60dc..6e34ff3 100644 --- a/tests/test_tracker_routing.py +++ b/tests/test_tracker_routing.py @@ -467,6 +467,7 @@ def test_tracker_logs_stream_progress_before_request_completes(): method="POST", ) response = urllib.request.urlopen(req, timeout=3.0) + assert response.headers["X-Accel-Buffering"] == "no" first_line = response.readline() assert first_line.startswith(b"data:") assert chunk_sent.wait(timeout=1.0) From 42d6fe2b1539a94c6b5826016626ca54946733f8 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Wed, 8 Jul 2026 20:48:12 +0300 Subject: [PATCH 2/3] chat UI --- .../tracker/meshnet_tracker/dashboard.html | 96 +++++++++++++++---- tests/test_dashboard.py | 34 +++++++ 2 files changed, 114 insertions(+), 16 deletions(-) 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.""" From aa7f4eb13b783eb0bc5a8dc2536179307075a694 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Wed, 8 Jul 2026 20:51:42 +0300 Subject: [PATCH 3/3] more chat UI --- packages/tracker/meshnet_tracker/dashboard.html | 12 +++++------- packages/tracker/meshnet_tracker/server.py | 14 +++++++++++++- tests/test_dashboard.py | 3 ++- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index 12076cc..bf82ea9 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -277,6 +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 short = (s, n=14) => { s = String(s); return s.length > n ? s.slice(0, 6) + "…" + s.slice(-5) : s; }; function modelAliasKey(value) { @@ -1114,12 +1115,8 @@ function findRoutingForModel(model, routing) { 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 chatModelHealthHp(servedCopies) { + return hp(servedCopies); } function chatModelTypicalTps(model, routing) { @@ -1136,7 +1133,7 @@ function chatModelOptionLabel(model, routing) { const base = model.name && model.name !== model.id ? `${model.name} (${model.id})` : model.id; - const health = chatModelHealthLabel(model.coverage); + const health = chatModelHealthHp(model.servedCopies); const speed = chatModelTypicalTps(model, routing); const speedText = speed === null ? "?" : `${tps(speed)} tok/s`; const suffix = model.recommended ? " · recommended" : ""; @@ -1644,6 +1641,7 @@ async function refresh() { recommended: Boolean(model.recommended), aliases: model.aliases || [], coverage: model.shard_coverage_percentage, + servedCopies: model.served_model_copies, })).filter(model => model.id); renderHive(raft); renderNodes(map); diff --git a/packages/tracker/meshnet_tracker/server.py b/packages/tracker/meshnet_tracker/server.py index ce57080..c433da0 100644 --- a/packages/tracker/meshnet_tracker/server.py +++ b/packages/tracker/meshnet_tracker/server.py @@ -2729,6 +2729,11 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): required_start, required_end, ) + served_copies = _served_model_copies( + model_nodes, + required_start, + required_end, + ) aliases = [name] hf_repo = preset.get("hf_repo") if hf_repo and hf_repo not in aliases: @@ -2748,6 +2753,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): "recommended": bool(preset.get("recommended", False)), "deployment": _deployment_summary(alive, preset), "shard_coverage_percentage": coverage, + "served_model_copies": served_copies, }) seen_ids.add(name) if hf_repo: @@ -2778,6 +2784,11 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): aliases = [model_id, *[name for name in short_names if name != model_id]] required_start = 0 required_end = max(node.num_layers for node in model_nodes) - 1 + coverage = _coverage_percentage( + model_nodes, + required_start, + required_end, + ) data.append({ "id": model_id, "object": "model", @@ -2787,7 +2798,8 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): "hf_repo": model_id if any(node.hf_repo == model_id for node in model_nodes) else None, "aliases": aliases, "metadata": _model_metadata_from_nodes(model_nodes), - "shard_coverage_percentage": _coverage_percentage( + "shard_coverage_percentage": coverage, + "served_model_copies": _served_model_copies( model_nodes, required_start, required_end, diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index affa31d..dd6e650 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -62,11 +62,12 @@ def test_dashboard_chat_model_selector_shows_health_and_speed(): finally: tracker.stop() - assert "chatModelHealthLabel" in html + assert "chatModelHealthHp" 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 def test_dashboard_chat_sessions_use_delegated_handlers():