diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index 8b9b94e..bbddc6c 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -295,7 +295,7 @@

Node pending payouts

admin login required

Settlement history

admin login required

Tracker hive

loading…
-

Model placement

admin login required
+

Model placement

Choose a model to load or release.
admin login required

All accounts (admin)

Strikes / bans / forfeitures

admin login required

Client balances

admin login required
@@ -1801,25 +1801,34 @@ async function requestSelectedModelLoad() { async function requestAdminModelLoad(model) { const result = await apiCall("/v1/models/load", "POST", { model, force: true }); - if (!result.ok) return alert(result.data.error || "model load request failed"); + if (!result.ok) return showAdminModelPlacementStatus(result.data.error || "model load request failed", true); + const assignment = result.data.assignment || {}; + showAdminModelPlacementStatus(`Load queued on ${short(assignment.node_id || "node")} for ${model}.`); await refreshActiveTab(true); } async function releaseAdminModel(model) { - if (!confirm(`Release ${model} from every serving node?`)) return; const result = await apiCall("/v1/models/release", "POST", { model }); - if (!result.ok) return alert(result.data.error || "model release request failed"); + if (!result.ok) return showAdminModelPlacementStatus(result.data.error || "model release request failed", true); + showAdminModelPlacementStatus(`Release queued for ${result.data.released || 0} node(s) serving ${model}.`); await refreshActiveTab(true); } +function showAdminModelPlacementStatus(message, isError) { + const status = $("admin-model-placement-status"); + status.textContent = message; + status.className = isError ? "bad" : "ok"; +} + function renderAdminModelPlacement(models, map) { const nodes = (map && map.nodes) || []; const rows = ((models && models.data) || []).map(model => { const aliases = new Set([model.id, model.hf_repo, ...(model.aliases || [])].filter(Boolean)); const serving = nodes.filter(node => aliases.has(node.model) || aliases.has(node.hf_repo)).length; - const downloaded = nodes.filter(node => (node.downloaded_models || []).some(item => aliases.has(item.model) || aliases.has(item.hf_repo))).length; - const actions = ` ` + - ``; + const downloaded = nodes.filter(node => aliases.has(node.model) || aliases.has(node.hf_repo) || + (node.downloaded_models || []).some(item => aliases.has(item.model) || aliases.has(item.hf_repo))).length; + const actions = ` ` + + ``; return [esc(model.name || model.id), String(serving), String(downloaded), actions]; }); $("admin-model-placement").innerHTML = rows.length @@ -1827,6 +1836,13 @@ function renderAdminModelPlacement(models, map) { : '
no model presets configured
'; } +$("admin-model-placement").addEventListener("click", event => { + const load = event.target.closest("[data-admin-model-load]"); + const release = event.target.closest("[data-admin-model-release]"); + if (load) void requestAdminModelLoad(load.dataset.adminModelLoad); + if (release) void releaseAdminModel(release.dataset.adminModelRelease); +}); + function chatAuthToken() { if (accountApiKeys.length) return accountApiKeys[0]; return null; diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index c379ec8..9492b12 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -111,6 +111,9 @@ def test_dashboard_exposes_admin_model_inventory_and_release_controls(): assert '"/v1/models/release"' in html assert "requestAdminModelLoad" in html assert "releaseAdminModel" in html + assert 'data-admin-model-load=' in html + assert 'data-admin-model-release=' in html + assert "admin-model-placement-status" in html def test_network_map_includes_node_friendly_name():