Files
pikasTech-HWLAB/web/hwlab-cloud-web/hwpod/app.js
T

61 lines
5.6 KiB
JavaScript

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 = `<div class="empty">${escapeHtml(error.message)}</div>`;
}
}
function renderResources() {
if (!state.resources.length) { resourcesEl.innerHTML = '<div class="empty">未发现 HWPOD 节点或设备。</div>'; 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 `<button class="resource" type="button" data-key="${escapeHtml(key)}" data-status="${escapeHtml(item.status || "unknown")}" aria-current="${state.selected === key}"><span class="rail"></span><span class="resource-copy"><strong>${escapeHtml(title)}</strong><span>${escapeHtml(subtitle)}</span></span><span class="badge">${escapeHtml(item.status || "unknown")}</span></button>`;
}).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 = '<div class="detail-empty"><strong>选择一个资源</strong><span>查看能力、占用状态和 blocker。</span></div>'; 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 `<section class="detail-block"><h3>${title}</h3>${body}</section>`; }
function kv(values) { return `<dl class="kv">${Object.entries(values).map(([key, value]) => `<dt>${escapeHtml(key)}</dt><dd>${escapeHtml(value ?? "-")}</dd>`).join("")}</dl>`; }
function chips(values) { const items = Array.isArray(values) ? values : []; return items.length ? `<div class="chips">${items.map(value => `<span class="chip">${escapeHtml(value)}</span>`).join("")}</div>` : '<span class="empty">未声明能力</span>'; }
function blockers(values) { const items = Array.isArray(values) ? values : []; return items.length ? detailBlock("BLOCKERS", items.map(value => `<div class="blocker"><strong>${escapeHtml(value.code || "blocked")}</strong><br>${escapeHtml(value.summary || value.message || "资源不可用")}</div>`).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 => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char])); }