const state = { resources: [], selected: null }; const resourcesEl = document.querySelector("[data-resources]"); const detailEl = document.querySelector("[data-detail]"); const healthEl = document.querySelector("[data-health]"); document.querySelector("[data-refresh]").addEventListener("click", refresh); void refresh(); async function refresh() { healthEl.dataset.health = "loading"; document.querySelector("[data-health-label]").textContent = "同步中"; try { const [health, nodes, devices] = await Promise.all([getJson("/health/ready"), getJson("/v1/hwpod/topology?resource=node&limit=100"), getJson("/v1/hwpod/topology?resource=device&limit=100")]); const nodeItems = Array.isArray(nodes.items) ? nodes.items : []; const deviceItems = Array.isArray(devices.items) ? devices.items : []; state.resources = [...nodeItems, ...deviceItems]; updateStats(nodes.summary || {}, devices.summary || {}); renderResources(); if (state.selected) selectResource(state.resources.find(item => resourceKey(item) === state.selected) || null); healthEl.dataset.health = health.ok ? "ready" : "blocked"; document.querySelector("[data-health-label]").textContent = health.ok ? "服务就绪" : "依赖阻塞"; document.querySelector("[data-observed]").textContent = displayTime(devices.observedAt || nodes.observedAt); } catch (error) { healthEl.dataset.health = "blocked"; document.querySelector("[data-health-label]").textContent = "连接失败"; resourcesEl.innerHTML = `
${escapeHtml(error.message)}
`; } } function renderResources() { if (!state.resources.length) { resourcesEl.innerHTML = '
未发现 HWPOD 节点或设备。
'; return; } resourcesEl.innerHTML = state.resources.map(item => { const key = resourceKey(item); const title = item.resourceType === "hwpod" ? item.name || item.hwpodId : item.name || item.nodeId; const subtitle = item.resourceType === "hwpod" ? `${item.nodeId || "未绑定节点"} · ${item.elements?.workspace?.label || "未声明工作区"}` : `${item.platform || "unknown"} · ${item.deviceCount || 0} 个 HWPOD`; return ``; }).join(""); resourcesEl.querySelectorAll("[data-key]").forEach(button => button.addEventListener("click", () => selectResource(state.resources.find(item => resourceKey(item) === button.dataset.key)))); } function selectResource(item) { state.selected = item ? resourceKey(item) : null; renderResources(); if (!item) { detailEl.innerHTML = '
选择一个资源查看能力、占用状态和 blocker。
'; return; } const identity = item.resourceType === "hwpod" ? { 类型: "HWPOD", ID: item.hwpodId, 节点: item.nodeId, 状态: item.status } : { 类型: "HWPOD Node", ID: item.nodeId, 平台: item.platform, 状态: item.status }; const capabilities = item.resourceType === "hwpod" ? item.declaredCapabilities : item.actualCapabilities; detailEl.innerHTML = `${detailBlock("IDENTITY", kv(identity))}${detailBlock("CAPABILITIES", chips(capabilities))}${blockers(item.blockers)}${detailBlock("RAW STATUS", kv({ 在线: String(Boolean(item.online)), 占用: String(Boolean(item.busy || item.inFlight?.busy)), 最近观测: displayTime(item.observedAt) }))}`; } function updateStats(nodes, devices) { setStat("nodes", nodes.nodeCount || 0); setStat("online", nodes.onlineNodeCount || 0); setStat("devices", devices.deviceCount || 0); setStat("available", devices.availableDeviceCount || 0); setStat("blocked", (devices.deviceCount || 0) - (devices.availableDeviceCount || 0)); } function setStat(name, value) { document.querySelector(`[data-stat="${name}"]`).textContent = String(value); } function detailBlock(title, body) { return `

${title}

${body}
`; } function kv(values) { return `
${Object.entries(values).map(([key, value]) => `
${escapeHtml(key)}
${escapeHtml(value ?? "-")}
`).join("")}
`; } function chips(values) { const items = Array.isArray(values) ? values : []; return items.length ? `
${items.map(value => `${escapeHtml(value)}`).join("")}
` : '未声明能力'; } function blockers(values) { const items = Array.isArray(values) ? values : []; return items.length ? detailBlock("BLOCKERS", items.map(value => `
${escapeHtml(value.code || "blocked")}
${escapeHtml(value.summary || value.message || "资源不可用")}
`).join("")) : ""; } function resourceKey(item) { return `${item.resourceType}:${item.hwpodId || item.nodeId}`; } function displayTime(value) { if (!value) return "尚未同步"; const date = new Date(value); return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString("zh-CN", { hour12: false }); } async function getJson(url) { const response = await fetch(url, { headers: { accept: "application/json" } }); const body = await response.json().catch(() => null); if (!response.ok || !body) throw new Error(body?.error?.message || `${url} 返回 HTTP ${response.status}`); return body; } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/gu, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); }