diff --git a/tests/test_static_vendor.py b/tests/test_static_vendor.py index 3479f7b..8c037e1 100644 --- a/tests/test_static_vendor.py +++ b/tests/test_static_vendor.py @@ -74,8 +74,12 @@ class StaticVendorTests(unittest.TestCase): self.assertNotIn('capabilities: ["origin.plot@v2"]', admin_js) self.assertIn("节点能力由客户端自动发现并上报", html) self.assertIn("runtime.capability_runtime", admin_js) + self.assertIn('item.health === "ready"', admin_js) self.assertIn("item.adapter_version", admin_js) self.assertIn("item.software_version", admin_js) + self.assertIn("可用能力 / 软件版本", admin_js) + self.assertNotIn("自动发现能力", admin_js) + self.assertIn(".node-capability { display: flex", html) self.assertIn('runtime.origin ? [["origin.plot@v2", runtime.origin]]', admin_js) self.assertIn("ttl_seconds: 600", admin_js) self.assertIn("navigator.clipboard.writeText(value)", admin_js) diff --git a/web/static/admin.html b/web/static/admin.html index c3420ba..5e7564c 100644 --- a/web/static/admin.html +++ b/web/static/admin.html @@ -181,7 +181,18 @@ .node-status.disabled { color: var(--danger); background: var(--accent-soft); } .node-name { font-weight: 600; } .node-id { color: var(--muted); font: 11px/1.4 var(--mono); } - .node-runtime { max-width: 280px; white-space: normal; color: var(--muted); } + .node-version { margin-top: 3px; color: var(--muted); font: 11px/1.4 var(--mono); } + .node-table { min-width: 820px; } + .node-table th:nth-child(1), .node-table td:nth-child(1) { width: 260px; } + .node-table th:nth-child(2), .node-table td:nth-child(2) { width: 80px; text-align: left; } + .node-table th:nth-child(3), .node-table td:nth-child(3) { text-align: left; } + .node-table th:nth-child(4), .node-table td:nth-child(4) { width: 90px; } + .node-table th:nth-child(5), .node-table td:nth-child(5) { width: 150px; } + .node-capabilities { min-width: 360px; } + .node-capability { display: flex; align-items: center; gap: 8px; min-width: max-content; } + .node-capability + .node-capability { margin-top: 5px; } + .node-capability-version { color: var(--muted); font: 11px/1.4 var(--mono); } + .node-empty-capability { color: var(--muted); } .node-actions { text-align: right; } #node-enrollment-modal .card { width: min(520px, calc(100vw - 32px)); margin: 0; padding: 0; } .node-modal-head, .node-modal-actions { diff --git a/web/static/js/admin.js b/web/static/js/admin.js index cf203c2..93c5792 100644 --- a/web/static/js/admin.js +++ b/web/static/js/admin.js @@ -178,52 +178,54 @@ function nodeSoftwareName(capability, item) { return item.software || capability; } +function nodeAvailableCapabilities(node, runtime) { + const reported = new Set(node.capabilities || []); + return nodeCapabilityRuntimes(runtime).filter(([capability, item]) => + item.health === "ready" && (!reported.size || reported.has(capability)) + ); +} + +function nodeCapabilityVersionHTML(capability, item) { + const name = nodeSoftwareName(capability, item); + const versions = [ + item.software_version ? `${name} ${item.software_version}` : name, + item.adapter_version ? `Adapter ${item.adapter_version}` : "", + ].filter(Boolean); + return `
` + + `${escapeHtml(capability)}` + + `${escapeHtml(versions.join(" · "))}` + + `
`; +} + function renderWindowsNodes() { const rows = softwareNodes.map(node => { const runtime = node.runtime || {}; - const capabilityRuntimes = nodeCapabilityRuntimes(runtime); - const runtimeParts = [ - node.os_version || "", - runtime.desktop_session === true ? "桌面会话" : "", - runtime.available_slots != null ? `可用槽位 ${runtime.available_slots}` : "", - ...capabilityRuntimes.map(([capability, item]) => { - const name = nodeSoftwareName(capability, item); - return `${name}${item.health === "ready" ? "可用" : "不可用"}`; - }), - ].filter(Boolean); - const versionParts = [ - node.node_version ? `Node ${node.node_version}` : "", - ...capabilityRuntimes.flatMap(([capability, item]) => { - const name = nodeSoftwareName(capability, item); - return [ - item.adapter_version ? `${name} Adapter ${item.adapter_version}` : "", - item.software_version ? `${name} ${item.software_version}` : "", - ]; - }), - ].filter(Boolean); + const availableCapabilities = nodeAvailableCapabilities(node, runtime); + const capabilityHTML = availableCapabilities + .map(([capability, item]) => nodeCapabilityVersionHTML(capability, item)) + .join(""); const lastSeen = node.last_seen_at ? `${escapeHtml(fmtTimeAgo(node.last_seen_at))}` : "—"; const disabled = node.status === "disabled"; return `` + `
${escapeHtml(node.name || "未命名节点")}
` - + `
${escapeHtml(node.node_id)}
` + + `
${escapeHtml(node.node_id)}
` + + `
${escapeHtml(node.node_version ? `Node ${node.node_version}` : "Node 版本未上报")}
` + `${nodeStatusHTML(node.status)}` - + `${escapeHtml(runtimeParts.join(" · ") || "—")}` - + `${escapeHtml(versionParts.join(" · ") || "—")}` - + `${(node.capabilities || []).map(x => `${escapeHtml(x)}`).join(" ") || "—"}` + + `${capabilityHTML || `暂无可用能力`}` + `${lastSeen}` + ` ` + `` + ``; - }).join("") || `尚无已注册的 Windows Node`; + }).join("") || `尚无已注册的 Windows Node`; $("s-windows-node").innerHTML = `
` + `

Windows Node(${softwareNodes.length})

查看节点状态;禁用会立即断开节点并拒绝后续连接。
` + `` - + `
` - + `${rows}
节点状态运行环境版本自动发现能力最近心跳操作
`; + + `
` + + `${rows}
节点 / Node 版本状态可用能力 / 软件版本最近心跳操作
`; $("node-enrollment-open").onclick = openNodeEnrollmentModal; $("s-windows-node").querySelectorAll("[data-node-toggle]").forEach(button => { button.onclick = () => toggleSoftwareNode(button);