diff --git a/packages/node/meshnet_node/hardware.py b/packages/node/meshnet_node/hardware.py
index 7c97a06..9bffa9a 100644
--- a/packages/node/meshnet_node/hardware.py
+++ b/packages/node/meshnet_node/hardware.py
@@ -2,6 +2,7 @@
import json
import os
+import shutil
import subprocess
import time
@@ -183,6 +184,17 @@ def with_forced_cpu(hw: dict) -> dict:
return forced
+def _with_model_drive(profile: dict) -> dict:
+ """Attach free space for the default model cache drive to tracker diagnostics."""
+ try:
+ cache_root = os.path.expanduser("~/.cache/meshnet/shards")
+ profile["model_drive_free_bytes"] = shutil.disk_usage(os.path.expanduser("~")).free
+ profile["model_drive_path"] = cache_root
+ except OSError:
+ pass
+ return profile
+
+
def detect_hardware() -> dict:
"""Detect GPU model and available VRAM. Returns hardware profile dict."""
ram_mb = _detect_ram_mb()
@@ -208,23 +220,23 @@ def detect_hardware() -> dict:
}
if torch_gpu is not None and torch_gpu.get("gcn_arch"):
profile["gcn_arch"] = torch_gpu["gcn_arch"]
- return profile
+ return _with_model_drive(profile)
except ImportError:
pass
torch_inventory = _gpu_inventory_profile(torch_gpu, ram_mb)
if torch_inventory is not None:
- return torch_inventory
+ return _with_model_drive(torch_inventory)
nvidia_gpu = _gpu_inventory_profile(_detect_nvidia_smi_gpu_memory(), ram_mb)
if nvidia_gpu is not None:
- return nvidia_gpu
+ return _with_model_drive(nvidia_gpu)
windows_gpu = _gpu_inventory_profile(_detect_windows_gpu_memory(), ram_mb)
if windows_gpu is not None:
- return windows_gpu
+ return _with_model_drive(windows_gpu)
- return {
+ return _with_model_drive({
"device": "cpu",
"gpu_name": None,
"vram_mb": 0,
@@ -232,7 +244,7 @@ def detect_hardware() -> dict:
"shared_vram_mb": 0,
"ram_mb": ram_mb,
"cuda_available": False,
- }
+ })
def benchmark_throughput_checked(device_str: str = "cpu") -> tuple[float, bool, str | None]:
diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html
index bbddc6c..ef4b282 100644
--- a/packages/tracker/meshnet_tracker/dashboard.html
+++ b/packages/tracker/meshnet_tracker/dashboard.html
@@ -296,6 +296,7 @@
Settlement history
admin login required
Model placement
Choose a model to load or release.
admin login required
+ Total node pool
admin login required
Strikes / bans / forfeitures
admin login required
Client balances
admin login required
@@ -1789,7 +1790,7 @@ 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 });
+ const result = await apiCall("/v1/models/load", "POST", { model: selectedChatModel, force: isAdmin });
if (button) button.disabled = false;
if (!result.ok) {
alert(result.data.error || "model load request failed");
@@ -1820,6 +1821,31 @@ function showAdminModelPlacementStatus(message, isError) {
status.className = isError ? "bad" : "ok";
}
+function gib(bytes) { return bytes == null ? "not reported" : `${(Number(bytes) / 1073741824).toFixed(1)} GiB`; }
+
+function renderAdminNodePool(map) {
+ const groups = {};
+ for (const node of (map && map.nodes) || []) {
+ const account = node.wallet_address || "unbound account";
+ (groups[account] = groups[account] || []).push(node);
+ }
+ let html = "";
+ for (const [account, nodes] of Object.entries(groups).sort(([a], [b]) => a.localeCompare(b))) {
+ html += `
${esc(short(account, 20))} ${nodes.length} node(s)
`;
+ html += table(["node", "assignment", "state / slots", "RAM", "GPU / VRAM", "model drive"], nodes.map(node => {
+ const hw = node.hardware_profile || {};
+ const cap = node.capacity || {};
+ const disk = hw.model_drive_free_bytes ?? hw.model_path_free_bytes ?? hw.disk_free_bytes;
+ const gpu = hw.gpu_name || (hw.cuda_available ? "CUDA GPU" : "CPU only");
+ return [nodeDisplayCell(node), esc(node.hf_repo || node.model || "unassigned"),
+ esc(`${node.stats?.status || "?"} · ${cap.loaded_slots ?? "?"}/${cap.max_loaded_shards ?? node.max_loaded_shards ?? "?"} slots`),
+ esc(gib(node.ram_bytes || (hw.ram_mb && hw.ram_mb * 1048576))),
+ esc(`${gpu} · ${gib(node.vram_bytes || (hw.vram_mb && hw.vram_mb * 1048576))}`), esc(gib(disk))];
+ }));
+ }
+ $("admin-node-pool").innerHTML = html || 'no nodes registered
';
+}
+
function renderAdminModelPlacement(models, map) {
const nodes = (map && map.nodes) || [];
const rows = ((models && models.data) || []).map(model => {
@@ -2490,6 +2516,7 @@ async function fetchAdminTab() {
renderIfChanged("billing-summary", summary, data => renderBilling(data));
renderIfChanged("fraud", { wallets, summary }, data => renderFraud(data.wallets, data.summary));
renderIfChanged("admin-model-placement", { models, map }, data => renderAdminModelPlacement(data.models, data.map));
+ renderIfChanged("admin-node-pool", map, renderAdminNodePool);
if (adminResp && adminResp.ok) {
renderIfChanged("admin", adminResp.data.accounts || [], accounts => {
const rows = accounts.map(a => {
diff --git a/packages/tracker/meshnet_tracker/server.py b/packages/tracker/meshnet_tracker/server.py
index 32c7239..1ea2144 100644
--- a/packages/tracker/meshnet_tracker/server.py
+++ b/packages/tracker/meshnet_tracker/server.py
@@ -3366,6 +3366,11 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
"endpoint": node.endpoint,
"relay_addr": node.relay_addr,
"peer_id": node.peer_id,
+ "wallet_address": node.wallet_address,
+ "hardware_profile": dict(node.hardware_profile),
+ "ram_bytes": node.ram_bytes,
+ "vram_bytes": node.vram_bytes,
+ "max_loaded_shards": node.max_loaded_shards,
}
for node in tracker_nodes
],
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 9492b12..d3c1234 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -114,6 +114,9 @@ def test_dashboard_exposes_admin_model_inventory_and_release_controls():
assert 'data-admin-model-load=' in html
assert 'data-admin-model-release=' in html
assert "admin-model-placement-status" in html
+ assert 'id="admin-node-pool"' in html
+ assert "renderAdminNodePool" in html
+ assert "model drive" in html
def test_network_map_includes_node_friendly_name():