merge: 修复 HWPOD 控制台 L0/L1 问题
This commit is contained in:
@@ -28,7 +28,7 @@ test("typed topology covers multi-device, offline, unconfigured, invalid, mismat
|
|||||||
runtimeKind: "python-tkinter-gui",
|
runtimeKind: "python-tkinter-gui",
|
||||||
installed: true,
|
installed: true,
|
||||||
desktopVisible: true,
|
desktopVisible: true,
|
||||||
capabilities: ["workspace.ls"],
|
capabilities: ["workspace.ls", "node.health"],
|
||||||
inFlightCount: 1,
|
inFlightCount: 1,
|
||||||
maxInFlight: 1,
|
maxInFlight: 1,
|
||||||
lastDiagnostic: { level: "ERROR", source: "ws", message: "connect failed: Bearer private-token?token=still-secret" },
|
lastDiagnostic: { level: "ERROR", source: "ws", message: "connect failed: Bearer private-token?token=still-secret" },
|
||||||
@@ -44,15 +44,24 @@ test("typed topology covers multi-device, offline, unconfigured, invalid, mismat
|
|||||||
assert.equal(devicePayload.summary.nodeCount, 3);
|
assert.equal(devicePayload.summary.nodeCount, 3);
|
||||||
assert.equal(devicePayload.summary.deviceCount, 3);
|
assert.equal(devicePayload.summary.deviceCount, 3);
|
||||||
assert.equal(devicePayload.summary.invalidSpecCount, 1);
|
assert.equal(devicePayload.summary.invalidSpecCount, 1);
|
||||||
assert.equal(devicePayload.items.find((item: any) => item.hwpodId === "hwpod-a").status, "busy");
|
const deviceA = devicePayload.items.find((item: any) => item.hwpodId === "hwpod-a");
|
||||||
assert.equal(devicePayload.items.find((item: any) => item.hwpodId === "hwpod-b").status, "capability-mismatch");
|
const deviceB = devicePayload.items.find((item: any) => item.hwpodId === "hwpod-b");
|
||||||
assert.deepEqual(devicePayload.items.find((item: any) => item.hwpodId === "hwpod-b").missingCapabilities, ["debug.download"]);
|
assert.equal(deviceA.status, "online");
|
||||||
|
assert.equal(deviceA.busy, true);
|
||||||
|
assert.equal(deviceA.available, false);
|
||||||
|
assert.equal(deviceB.status, "online");
|
||||||
|
assert.equal(deviceB.capabilityMismatch, true);
|
||||||
|
assert.deepEqual(deviceB.missingCapabilities, ["debug.download"]);
|
||||||
|
assert.deepEqual(deviceB.capabilityCoverage, { matched: 1, declared: 2, actual: 2, missing: 1, extra: 1 });
|
||||||
|
assert.equal(deviceB.blockers.some((item: any) => item.code === "hwpod_capability_mismatch"), true);
|
||||||
assert.equal(devicePayload.items.find((item: any) => item.hwpodId === "hwpod-c").status, "offline");
|
assert.equal(devicePayload.items.find((item: any) => item.hwpodId === "hwpod-c").status, "offline");
|
||||||
|
|
||||||
const nodePayload = buildHwpodTopologyReadModel(specs, registry, { resource: "node", observedAt: "2026-07-13T01:00:00.000Z" });
|
const nodePayload = buildHwpodTopologyReadModel(specs, registry, { resource: "node", observedAt: "2026-07-13T01:00:00.000Z" });
|
||||||
const nodeA = nodePayload.items.find((item: any) => item.nodeId === "node-a");
|
const nodeA = nodePayload.items.find((item: any) => item.nodeId === "node-a");
|
||||||
assert.equal(nodeA.deviceCount, 2);
|
assert.equal(nodeA.deviceCount, 2);
|
||||||
|
assert.equal(nodeA.status, "online");
|
||||||
assert.equal(nodeA.inFlight.busy, true);
|
assert.equal(nodeA.inFlight.busy, true);
|
||||||
|
assert.deepEqual(nodeA.capabilityCoverage, { matched: 1, declared: 2, actual: 2, missing: 1, extra: 1 });
|
||||||
assert.equal(nodeA.diagnostic.message.includes("private-token"), false);
|
assert.equal(nodeA.diagnostic.message.includes("private-token"), false);
|
||||||
assert.equal(nodeA.diagnostic.message.includes("still-secret"), false);
|
assert.equal(nodeA.diagnostic.message.includes("still-secret"), false);
|
||||||
assert.equal(nodeA.diagnostic.message.includes("[REDACTED]"), true);
|
assert.equal(nodeA.diagnostic.message.includes("[REDACTED]"), true);
|
||||||
@@ -61,7 +70,8 @@ test("typed topology covers multi-device, offline, unconfigured, invalid, mismat
|
|||||||
assert.equal(nodeA.readiness.find((stage: any) => stage.id === "desktop-visible").nextAction, null);
|
assert.equal(nodeA.readiness.find((stage: any) => stage.id === "desktop-visible").nextAction, null);
|
||||||
assert.equal(nodeA.readiness.find((stage: any) => stage.id === "hwpod-ready").nextAction.entrypoint, "hwpod-topology");
|
assert.equal(nodeA.readiness.find((stage: any) => stage.id === "hwpod-ready").nextAction.entrypoint, "hwpod-topology");
|
||||||
assert.equal(nodePayload.items.find((item: any) => item.nodeId === "node-b").readiness.find((stage: any) => stage.id === "workspace-ready").blocker.code, "hwpod_node_spec_missing");
|
assert.equal(nodePayload.items.find((item: any) => item.nodeId === "node-b").readiness.find((stage: any) => stage.id === "workspace-ready").blocker.code, "hwpod_node_spec_missing");
|
||||||
assert.equal(nodePayload.items.find((item: any) => item.nodeId === "node-b").status, "unconfigured");
|
assert.equal(nodePayload.items.find((item: any) => item.nodeId === "node-b").status, "online");
|
||||||
|
assert.equal(nodePayload.summary.unconfiguredNodeCount, 1);
|
||||||
assert.equal(nodePayload.items.find((item: any) => item.nodeId === "node-c").status, "offline");
|
assert.equal(nodePayload.items.find((item: any) => item.nodeId === "node-c").status, "offline");
|
||||||
assert.equal(nodePayload.invalidSpecs[0].blocker.code, "invalid_hwpod_spec");
|
assert.equal(nodePayload.invalidSpecs[0].blocker.code, "invalid_hwpod_spec");
|
||||||
});
|
});
|
||||||
@@ -74,11 +84,11 @@ test("typed topology keeps server-side status filtering and cursor bounded", ()
|
|||||||
{ nodeId: "node-b", status: "online", capabilities: ["workspace.ls"], inFlightCount: 0, maxInFlight: 1 }
|
{ nodeId: "node-b", status: "online", capabilities: ["workspace.ls"], inFlightCount: 0, maxInFlight: 1 }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
const first = buildHwpodTopologyReadModel(specs, registry, { resource: "device", status: "available", limit: 1 });
|
const first = buildHwpodTopologyReadModel(specs, registry, { resource: "device", status: "online", limit: 1 });
|
||||||
assert.equal(first.items.length, 1);
|
assert.equal(first.items.length, 1);
|
||||||
assert.equal(first.page.total, 2);
|
assert.equal(first.page.total, 2);
|
||||||
assert.ok(first.page.nextCursor);
|
assert.ok(first.page.nextCursor);
|
||||||
const second = buildHwpodTopologyReadModel(specs, registry, { resource: "device", status: "available", limit: 1, cursor: first.page.nextCursor });
|
const second = buildHwpodTopologyReadModel(specs, registry, { resource: "device", status: "online", limit: 1, cursor: first.page.nextCursor });
|
||||||
assert.equal(second.items.length, 1);
|
assert.equal(second.items.length, 1);
|
||||||
assert.notEqual(first.items[0].hwpodId, second.items[0].hwpodId);
|
assert.notEqual(first.items[0].hwpodId, second.items[0].hwpodId);
|
||||||
assert.equal(second.page.previousCursor, "b2Zmc2V0OjA");
|
assert.equal(second.page.previousCursor, "b2Zmc2V0OjA");
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ export function buildHwpodTopologyReadModel(specs: any[], registrySnapshot: any
|
|||||||
nodeCount: nodes.length,
|
nodeCount: nodes.length,
|
||||||
onlineNodeCount: nodes.filter((node) => node.online).length,
|
onlineNodeCount: nodes.filter((node) => node.online).length,
|
||||||
offlineNodeCount: nodes.filter((node) => !node.online).length,
|
offlineNodeCount: nodes.filter((node) => !node.online).length,
|
||||||
unconfiguredNodeCount: nodes.filter((node) => node.status === "unconfigured").length,
|
unconfiguredNodeCount: nodes.filter((node) => node.deviceCount === 0).length,
|
||||||
deviceCount: devices.length,
|
deviceCount: devices.length,
|
||||||
availableDeviceCount: devices.filter((device) => device.status === "available").length,
|
availableDeviceCount: devices.filter((device) => device.available).length,
|
||||||
busyDeviceCount: devices.filter((device) => device.busy).length,
|
busyDeviceCount: devices.filter((device) => device.busy).length,
|
||||||
capabilityMismatchCount: devices.filter((device) => device.capabilityMismatch).length,
|
capabilityMismatchCount: devices.filter((device) => device.capabilityMismatch).length,
|
||||||
invalidSpecCount: invalidSpecs.length
|
invalidSpecCount: invalidSpecs.length
|
||||||
@@ -105,12 +105,14 @@ function buildDevice(spec: any, node: any, observedAt: string) {
|
|||||||
const declaredCapabilities = declaredCapabilitiesFor(body);
|
const declaredCapabilities = declaredCapabilitiesFor(body);
|
||||||
const actualCapabilities = node?.actualCapabilities ?? [];
|
const actualCapabilities = node?.actualCapabilities ?? [];
|
||||||
const missingCapabilities = node ? declaredCapabilities.filter((capability) => !actualCapabilities.includes(capability)) : declaredCapabilities;
|
const missingCapabilities = node ? declaredCapabilities.filter((capability) => !actualCapabilities.includes(capability)) : declaredCapabilities;
|
||||||
|
const capabilityCoverage = buildCapabilityCoverage(declaredCapabilities, actualCapabilities);
|
||||||
const availability = availabilitySummary(spec.availability);
|
const availability = availabilitySummary(spec.availability);
|
||||||
const online = Boolean(node?.online);
|
const online = Boolean(node?.online);
|
||||||
const busy = Boolean(node?.busy);
|
const busy = Boolean(node?.busy);
|
||||||
const capabilityMismatch = online && missingCapabilities.length > 0;
|
const capabilityMismatch = online && missingCapabilities.length > 0;
|
||||||
const blockers = deviceBlockers({ online, busy, capabilityMismatch, missingCapabilities, availability });
|
const blockers = deviceBlockers({ online, busy, capabilityMismatch, missingCapabilities, availability });
|
||||||
const status = deviceStatus({ online, busy, capabilityMismatch, availability });
|
const status = online ? "online" : "offline";
|
||||||
|
const available = online && !busy && !capabilityMismatch && availability.ok === true;
|
||||||
return {
|
return {
|
||||||
resourceType: "hwpod",
|
resourceType: "hwpod",
|
||||||
hwpodId: text(spec.hwpodId ?? spec.name),
|
hwpodId: text(spec.hwpodId ?? spec.name),
|
||||||
@@ -120,12 +122,13 @@ function buildDevice(spec: any, node: any, observedAt: string) {
|
|||||||
nodeName: node?.name ?? text(spec.nodeId),
|
nodeName: node?.name ?? text(spec.nodeId),
|
||||||
status,
|
status,
|
||||||
online,
|
online,
|
||||||
available: status === "available",
|
available,
|
||||||
busy,
|
busy,
|
||||||
capabilityMismatch,
|
capabilityMismatch,
|
||||||
declaredCapabilities,
|
declaredCapabilities,
|
||||||
actualCapabilities,
|
actualCapabilities,
|
||||||
missingCapabilities,
|
missingCapabilities,
|
||||||
|
capabilityCoverage,
|
||||||
elements: hwpodElements(body),
|
elements: hwpodElements(body),
|
||||||
availability,
|
availability,
|
||||||
operation: node?.latestOperation?.hwpodId === text(spec.hwpodId ?? spec.name) ? node.latestOperation : null,
|
operation: node?.latestOperation?.hwpodId === text(spec.hwpodId ?? spec.name) ? node.latestOperation : null,
|
||||||
@@ -144,11 +147,12 @@ function buildNode(nodeId: string, onlineNode: any, devices: any[], observedAt:
|
|||||||
const declaredCapabilities = uniqueStrings(devices.flatMap((device) => device.declaredCapabilities));
|
const declaredCapabilities = uniqueStrings(devices.flatMap((device) => device.declaredCapabilities));
|
||||||
const actualCapabilities = onlineNode?.actualCapabilities ?? [];
|
const actualCapabilities = onlineNode?.actualCapabilities ?? [];
|
||||||
const missingCapabilities = onlineNode ? declaredCapabilities.filter((capability) => !actualCapabilities.includes(capability)) : declaredCapabilities;
|
const missingCapabilities = onlineNode ? declaredCapabilities.filter((capability) => !actualCapabilities.includes(capability)) : declaredCapabilities;
|
||||||
|
const capabilityCoverage = buildCapabilityCoverage(declaredCapabilities, actualCapabilities);
|
||||||
const busy = Boolean(onlineNode?.busy);
|
const busy = Boolean(onlineNode?.busy);
|
||||||
const online = Boolean(onlineNode?.online);
|
const online = Boolean(onlineNode?.online);
|
||||||
const capabilityMismatch = online && missingCapabilities.length > 0;
|
const capabilityMismatch = online && missingCapabilities.length > 0;
|
||||||
const hasSpec = devices.length > 0;
|
const hasSpec = devices.length > 0;
|
||||||
const status = !online ? "offline" : !hasSpec ? "unconfigured" : capabilityMismatch ? "capability-mismatch" : busy ? "busy" : "online";
|
const status = online ? "online" : "offline";
|
||||||
const blockers = nodeBlockers({ online, hasSpec, busy, capabilityMismatch, missingCapabilities, devices });
|
const blockers = nodeBlockers({ online, hasSpec, busy, capabilityMismatch, missingCapabilities, devices });
|
||||||
const workspaceReady = devices.some((device) => device.availability.ok === true);
|
const workspaceReady = devices.some((device) => device.availability.ok === true);
|
||||||
const hwpodReady = devices.some((device) => device.available);
|
const hwpodReady = devices.some((device) => device.available);
|
||||||
@@ -175,6 +179,7 @@ function buildNode(nodeId: string, onlineNode: any, devices: any[], observedAt:
|
|||||||
declaredCapabilities,
|
declaredCapabilities,
|
||||||
actualCapabilities,
|
actualCapabilities,
|
||||||
missingCapabilities,
|
missingCapabilities,
|
||||||
|
capabilityCoverage,
|
||||||
capabilityMismatch,
|
capabilityMismatch,
|
||||||
inFlight: {
|
inFlight: {
|
||||||
count: onlineNode?.inFlightCount ?? 0,
|
count: onlineNode?.inFlightCount ?? 0,
|
||||||
@@ -244,13 +249,16 @@ function availabilitySummary(value: any) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function deviceStatus(input: any) {
|
function buildCapabilityCoverage(declaredCapabilities: string[], actualCapabilities: string[]) {
|
||||||
if (!input.online) return "offline";
|
const matched = declaredCapabilities.filter((capability) => actualCapabilities.includes(capability)).length;
|
||||||
if (input.capabilityMismatch) return "capability-mismatch";
|
const extra = actualCapabilities.filter((capability) => !declaredCapabilities.includes(capability)).length;
|
||||||
if (input.busy) return "busy";
|
return {
|
||||||
if (input.availability.ok === false) return "workspace-blocked";
|
matched,
|
||||||
if (input.availability.ok === true) return "available";
|
declared: declaredCapabilities.length,
|
||||||
return "unprobed";
|
actual: actualCapabilities.length,
|
||||||
|
missing: declaredCapabilities.length - matched,
|
||||||
|
extra
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function deviceBlockers(input: any) {
|
function deviceBlockers(input: any) {
|
||||||
|
|||||||
@@ -351,6 +351,24 @@ test("R2 visual constraints keep industrial controls bounded and component-owned
|
|||||||
assert.match(read(relativePath), /ViewModeSwitch/u, `${relativePath} should use the shared view switch`);
|
assert.match(read(relativePath), /ViewModeSwitch/u, `${relativePath} should use the shared view switch`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("HWPOD topology keeps connection state separate and owns bounded scrolling", () => {
|
||||||
|
const routes = read("src/router/index.ts");
|
||||||
|
for (const path of ["/hwpods/devices/:hwpodId?", "/hwpods/nodes/:nodeId?", "/hwpods/onboarding/:nodeId?"]) {
|
||||||
|
const route = routes.split(`{ path: "${path}"`)[1]?.split("},")[0] ?? "";
|
||||||
|
assert.match(route, /boundedLayout: true/u, `${path} should use bounded AppShell layout`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = read("src/views/admin/HwpodGroupsView.vue");
|
||||||
|
assert.match(view, /\.hwpod-console \{[^}]*height: 100%;[^}]*overflow: hidden;/u);
|
||||||
|
assert.match(view, /\.hwpod-workspace \{[^}]*flex: 1 1 auto;/u);
|
||||||
|
assert.doesNotMatch(view, /\.hwpod-workspace \{[^}]*height: clamp\(/u);
|
||||||
|
|
||||||
|
const deviceWorkspace = read("src/components/hwpod/HwpodDevicesWorkspace.vue");
|
||||||
|
assert.match(deviceWorkspace, /capabilityCoverage\.matched/u);
|
||||||
|
assert.match(deviceWorkspace, /capabilityCoverage\.declared/u);
|
||||||
|
assert.doesNotMatch(deviceWorkspace, /actualCapabilities\.length\s*\}\}\/\{\{\s*row\.declaredCapabilities\.length/u);
|
||||||
|
});
|
||||||
|
|
||||||
test("R6 Sub2API source comparison is recorded in parent/child issue scope", () => {
|
test("R6 Sub2API source comparison is recorded in parent/child issue scope", () => {
|
||||||
const issueBody = readIssueBodyFixture();
|
const issueBody = readIssueBodyFixture();
|
||||||
assert.match(issueBody, /Wei-Shaw\/sub2api@v0\.1\.136=a2f76e4/u);
|
assert.match(issueBody, /Wei-Shaw\/sub2api@v0\.1\.136=a2f76e4/u);
|
||||||
|
|||||||
@@ -14,12 +14,8 @@ const props = withDefaults(
|
|||||||
const emit = defineEmits<{ select: [id: string] }>();
|
const emit = defineEmits<{ select: [id: string] }>();
|
||||||
|
|
||||||
const statusLabel = computed(() => ({
|
const statusLabel = computed(() => ({
|
||||||
available: "可用",
|
online: "在线",
|
||||||
busy: "执行中",
|
|
||||||
offline: "节点离线",
|
offline: "节点离线",
|
||||||
"capability-mismatch": "能力不匹配",
|
|
||||||
"workspace-blocked": "工作区阻塞",
|
|
||||||
unprobed: "待探测"
|
|
||||||
}[props.device.status] ?? props.device.status));
|
}[props.device.status] ?? props.device.status));
|
||||||
|
|
||||||
function linkState(kind: "target" | "workspace" | "debug" | "io"): string {
|
function linkState(kind: "target" | "workspace" | "debug" | "io"): string {
|
||||||
@@ -80,7 +76,7 @@ function linkState(kind: "target" | "workspace" | "debug" | "io"): string {
|
|||||||
|
|
||||||
<p v-if="device.blockers[0]" class="pcb-blocker"><strong>{{ device.blockers[0].code }}</strong>{{ device.blockers[0].summary }}</p>
|
<p v-if="device.blockers[0]" class="pcb-blocker"><strong>{{ device.blockers[0].code }}</strong>{{ device.blockers[0].summary }}</p>
|
||||||
<footer>
|
<footer>
|
||||||
<span>{{ device.declaredCapabilities.length }} 项声明能力</span>
|
<span>能力覆盖 {{ device.capabilityCoverage.matched }}/{{ device.capabilityCoverage.declared }} · 上报 {{ device.capabilityCoverage.actual }}</span>
|
||||||
<span v-if="device.operation">{{ device.operation.status }} · {{ device.operation.opCount }} ops</span>
|
<span v-if="device.operation">{{ device.operation.status }} · {{ device.operation.opCount }} ops</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -95,17 +91,13 @@ function linkState(kind: "target" | "workspace" | "debug" | "io"): string {
|
|||||||
.pcb-card { position: relative; min-width: 0; border: 1px solid #cbd2cf; border-top: 3px solid #2e6b67; border-radius: 8px; background: #fbfaf6; box-shadow: 0 12px 28px rgb(20 34 32 / 8%); overflow: hidden; transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; }
|
.pcb-card { position: relative; min-width: 0; border: 1px solid #cbd2cf; border-top: 3px solid #2e6b67; border-radius: 8px; background: #fbfaf6; box-shadow: 0 12px 28px rgb(20 34 32 / 8%); overflow: hidden; transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; }
|
||||||
.pcb-card[data-selected="true"] { border-color: #1f7469; border-top-color: #145f57; box-shadow: 0 0 0 3px rgb(31 116 105 / 18%), 0 16px 34px rgb(20 55 48 / 14%); transform: translateY(-1px); }
|
.pcb-card[data-selected="true"] { border-color: #1f7469; border-top-color: #145f57; box-shadow: 0 0 0 3px rgb(31 116 105 / 18%), 0 16px 34px rgb(20 55 48 / 14%); transform: translateY(-1px); }
|
||||||
.pcb-card[data-selected="true"] footer button { border-color: #145f57; background: #dff2ed; color: #145f57; }
|
.pcb-card[data-selected="true"] footer button { border-color: #145f57; background: #dff2ed; color: #145f57; }
|
||||||
.pcb-card[data-status="busy"] { border-top-color: #b86d1d; }
|
.pcb-card[data-status="offline"] { border-top-color: #8d453b; }
|
||||||
.pcb-card[data-status="offline"], .pcb-card[data-status="workspace-blocked"] { border-top-color: #8d453b; }
|
|
||||||
.pcb-card[data-status="capability-mismatch"] { border-top-color: #8a6b16; }
|
|
||||||
.pcb-card__header { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; padding: 18px 20px 12px; }
|
.pcb-card__header { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; padding: 18px 20px 12px; }
|
||||||
.pcb-card__header h2 { margin: 3px 0 0; color: #152421; font-size: 1.08rem; letter-spacing: 0; overflow-wrap: anywhere; }
|
.pcb-card__header h2 { margin: 3px 0 0; color: #152421; font-size: 1.08rem; letter-spacing: 0; overflow-wrap: anywhere; }
|
||||||
.pcb-card__eyebrow { color: #687572; font: 600 .72rem/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
|
.pcb-card__eyebrow { color: #687572; font: 600 .72rem/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
|
||||||
.pcb-status { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; padding: 5px 9px; border: 1px solid #b7c6c1; border-radius: 999px; color: #244943; background: #eef7f2; font-size: .76rem; font-weight: 700; }
|
.pcb-status { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; padding: 5px 9px; border: 1px solid #b7c6c1; border-radius: 999px; color: #244943; background: #eef7f2; font-size: .76rem; font-weight: 700; }
|
||||||
.pcb-status i { width: 7px; height: 7px; border-radius: 50%; background: currentcolor; }
|
.pcb-status i { width: 7px; height: 7px; border-radius: 50%; background: currentcolor; }
|
||||||
.pcb-status[data-status="busy"] { color: #8b4c0a; background: #fff4df; border-color: #e6bd80; }
|
.pcb-status[data-status="offline"] { color: #7b332c; background: #fff0ed; border-color: #ddb2ac; }
|
||||||
.pcb-status[data-status="offline"], .pcb-status[data-status="workspace-blocked"] { color: #7b332c; background: #fff0ed; border-color: #ddb2ac; }
|
|
||||||
.pcb-status[data-status="capability-mismatch"] { color: #6f560b; background: #fff8d8; border-color: #d8c376; }
|
|
||||||
.pcb-map { display: block; width: calc(100% - 28px); max-height: 220px; margin: 0 14px; }
|
.pcb-map { display: block; width: calc(100% - 28px); max-height: 220px; margin: 0 14px; }
|
||||||
.pcb-board { fill: #142c28; stroke: #31514a; stroke-width: 2; }
|
.pcb-board { fill: #142c28; stroke: #31514a; stroke-width: 2; }
|
||||||
.pcb-grid-line { fill: none; stroke: rgb(137 184 168 / 9%); stroke-width: 1; }
|
.pcb-grid-line { fill: none; stroke: rgb(137 184 168 / 9%); stroke-width: 1; }
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import HwpodDeviceCard from "./HwpodDeviceCard.vue";
|
|||||||
defineProps<{ devices: HwpodDeviceTopologyItem[]; selectedId: string | null; view: "cards" | "list"; density: "compact" | "comfortable"; sort: string }>();
|
defineProps<{ devices: HwpodDeviceTopologyItem[]; selectedId: string | null; view: "cards" | "list"; density: "compact" | "comfortable"; sort: string }>();
|
||||||
const emit = defineEmits<{ select: [device: HwpodDeviceTopologyItem]; sort: [key: string] }>();
|
const emit = defineEmits<{ select: [device: HwpodDeviceTopologyItem]; sort: [key: string] }>();
|
||||||
const columns: DataGridColumn[] = [{ key: "name", label: "HWPOD", sortable: true, width: "190px" }, { key: "nodeName", label: "Node", width: "170px" }, { key: "status", label: "状态", sortable: true, width: "100px" }, { key: "elements", label: "目标 / 工作区", width: "230px" }, { key: "debugIo", label: "调试 / IO", width: "210px" }, { key: "capabilities", label: "能力", width: "140px" }, { key: "blocker", label: "主要 blocker", width: "220px" }, { key: "operation", label: "用户操作", width: "160px" }];
|
const columns: DataGridColumn[] = [{ key: "name", label: "HWPOD", sortable: true, width: "190px" }, { key: "nodeName", label: "Node", width: "170px" }, { key: "status", label: "状态", sortable: true, width: "100px" }, { key: "elements", label: "目标 / 工作区", width: "230px" }, { key: "debugIo", label: "调试 / IO", width: "210px" }, { key: "capabilities", label: "能力", width: "140px" }, { key: "blocker", label: "主要 blocker", width: "220px" }, { key: "operation", label: "用户操作", width: "160px" }];
|
||||||
const statusLabel = (status: string): string => ({ online: "在线", offline: "离线", busy: "忙碌", available: "可用", error: "异常", mismatch: "能力不匹配", "capability-mismatch": "能力不匹配" }[status] || status || "unknown");
|
const statusLabel = (status: string): string => ({ online: "在线", offline: "离线" }[status] || status || "unknown");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -49,7 +49,7 @@ const statusLabel = (status: string): string => ({ online: "在线", offline: "
|
|||||||
<template #cell-status="{ row }"><span class="status-chip" :data-status="row.status">{{ statusLabel(row.status) }}</span></template>
|
<template #cell-status="{ row }"><span class="status-chip" :data-status="row.status">{{ statusLabel(row.status) }}</span></template>
|
||||||
<template #cell-elements="{ row }"><span class="grid-stack"><strong>{{ row.elements.target.label }}</strong><small>{{ row.elements.workspace.label }}</small></span></template>
|
<template #cell-elements="{ row }"><span class="grid-stack"><strong>{{ row.elements.target.label }}</strong><small>{{ row.elements.workspace.label }}</small></span></template>
|
||||||
<template #cell-debugIo="{ row }"><span class="grid-stack"><strong>{{ row.elements.debugProbe.label }}</strong><small>{{ row.elements.ioProbe.label }}</small></span></template>
|
<template #cell-debugIo="{ row }"><span class="grid-stack"><strong>{{ row.elements.debugProbe.label }}</strong><small>{{ row.elements.ioProbe.label }}</small></span></template>
|
||||||
<template #cell-capabilities="{ row }"><span class="grid-stack"><strong>{{ row.actualCapabilities.length }}/{{ row.declaredCapabilities.length }}</strong><small v-if="row.missingCapabilities.length">缺 {{ row.missingCapabilities.join(', ') }}</small></span></template>
|
<template #cell-capabilities="{ row }"><span class="grid-stack"><strong>覆盖 {{ row.capabilityCoverage.matched }}/{{ row.capabilityCoverage.declared }}</strong><small>上报 {{ row.capabilityCoverage.actual }} · 缺 {{ row.capabilityCoverage.missing }} · 额外 {{ row.capabilityCoverage.extra }}</small></span></template>
|
||||||
<template #cell-blocker="{ row }"><span class="grid-stack"><strong>{{ row.blockers[0]?.code || '无' }}</strong><small>{{ row.blockers[0]?.summary || '当前可操作' }}</small></span></template>
|
<template #cell-blocker="{ row }"><span class="grid-stack"><strong>{{ row.blockers[0]?.code || '无' }}</strong><small>{{ row.blockers[0]?.summary || '当前可操作' }}</small></span></template>
|
||||||
<template #cell-operation="{ row }"><span class="grid-stack"><strong>{{ row.operation?.status || '无' }}</strong><small>{{ row.operation ? `${row.operation.opCount} ops` : '尚无用户操作' }}</small></span></template>
|
<template #cell-operation="{ row }"><span class="grid-stack"><strong>{{ row.operation?.status || '无' }}</strong><small>{{ row.operation ? `${row.operation.opCount} ops` : '尚无用户操作' }}</small></span></template>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import HwpodReadinessRail from "./HwpodReadinessRail.vue";
|
|||||||
defineProps<{ nodes: HwpodNodeTopologyItem[]; selectedId: string | null; view: "cards" | "list"; density: "compact" | "comfortable"; sort: string }>();
|
defineProps<{ nodes: HwpodNodeTopologyItem[]; selectedId: string | null; view: "cards" | "list"; density: "compact" | "comfortable"; sort: string }>();
|
||||||
const emit = defineEmits<{ select: [node: HwpodNodeTopologyItem]; sort: [key: string] }>();
|
const emit = defineEmits<{ select: [node: HwpodNodeTopologyItem]; sort: [key: string] }>();
|
||||||
const columns: DataGridColumn[] = [{ key: "name", label: "Node", sortable: true, width: "210px" }, { key: "status", label: "连接", sortable: true, width: "100px" }, { key: "deviceCount", label: "HWPOD", align: "right", width: "80px" }, { key: "inFlight", label: "In-flight", width: "100px" }, { key: "capabilities", label: "能力", width: "130px" }, { key: "mounted", label: "挂载 HWPOD", width: "220px" }, { key: "readiness", label: "接入阶段", width: "180px" }, { key: "lastSeen", label: "最后心跳", width: "150px" }, { key: "blocker", label: "Blocker", width: "220px" }];
|
const columns: DataGridColumn[] = [{ key: "name", label: "Node", sortable: true, width: "210px" }, { key: "status", label: "连接", sortable: true, width: "100px" }, { key: "deviceCount", label: "HWPOD", align: "right", width: "80px" }, { key: "inFlight", label: "In-flight", width: "100px" }, { key: "capabilities", label: "能力", width: "130px" }, { key: "mounted", label: "挂载 HWPOD", width: "220px" }, { key: "readiness", label: "接入阶段", width: "180px" }, { key: "lastSeen", label: "最后心跳", width: "150px" }, { key: "blocker", label: "Blocker", width: "220px" }];
|
||||||
const statusLabel = (status: string): string => ({ online: "在线", offline: "离线", busy: "忙碌", available: "可用", error: "异常", mismatch: "能力不匹配", "capability-mismatch": "能力不匹配" }[status] || status || "unknown");
|
const statusLabel = (status: string): string => ({ online: "在线", offline: "离线" }[status] || status || "unknown");
|
||||||
function formatTime(value: string | null): string { if (!value) return "-"; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); }
|
function formatTime(value: string | null): string { if (!value) return "-"; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ function formatTime(value: string | null): string { if (!value) return "-"; cons
|
|||||||
<section class="node-gauges">
|
<section class="node-gauges">
|
||||||
<div><small>HWPOD</small><strong>{{ node.deviceCount }}</strong></div>
|
<div><small>HWPOD</small><strong>{{ node.deviceCount }}</strong></div>
|
||||||
<div><small>in-flight</small><strong>{{ node.inFlight.count }}/{{ node.inFlight.max }}</strong></div>
|
<div><small>in-flight</small><strong>{{ node.inFlight.count }}/{{ node.inFlight.max }}</strong></div>
|
||||||
<div><small>capabilities</small><strong>{{ node.actualCapabilities.length }}/{{ node.declaredCapabilities.length }}</strong></div>
|
<div><small>capability coverage</small><strong>{{ node.capabilityCoverage.matched }}/{{ node.capabilityCoverage.declared }}</strong></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="mounted-hwpods" aria-label="挂载 HWPOD">
|
<section class="mounted-hwpods" aria-label="挂载 HWPOD">
|
||||||
<header><strong>挂载 HWPOD</strong><span>{{ node.hwpods.length }}</span></header>
|
<header><strong>挂载 HWPOD</strong><span>{{ node.hwpods.length }}</span></header>
|
||||||
@@ -85,7 +85,7 @@ function formatTime(value: string | null): string { if (!value) return "-"; cons
|
|||||||
<template #cell-status="{ row }"><span class="status-chip" :data-status="row.status">{{ statusLabel(row.status) }}</span></template>
|
<template #cell-status="{ row }"><span class="status-chip" :data-status="row.status">{{ statusLabel(row.status) }}</span></template>
|
||||||
<template #cell-deviceCount="{ row }"><strong>{{ row.deviceCount }}</strong></template>
|
<template #cell-deviceCount="{ row }"><strong>{{ row.deviceCount }}</strong></template>
|
||||||
<template #cell-inFlight="{ row }"><strong>{{ row.inFlight.count }}/{{ row.inFlight.max }}</strong></template>
|
<template #cell-inFlight="{ row }"><strong>{{ row.inFlight.count }}/{{ row.inFlight.max }}</strong></template>
|
||||||
<template #cell-capabilities="{ row }"><span class="grid-stack"><strong>{{ row.actualCapabilities.length }}/{{ row.declaredCapabilities.length }}</strong><small v-if="row.missingCapabilities.length">缺 {{ row.missingCapabilities.join(', ') }}</small></span></template>
|
<template #cell-capabilities="{ row }"><span class="grid-stack"><strong>覆盖 {{ row.capabilityCoverage.matched }}/{{ row.capabilityCoverage.declared }}</strong><small>上报 {{ row.capabilityCoverage.actual }} · 缺 {{ row.capabilityCoverage.missing }} · 额外 {{ row.capabilityCoverage.extra }}</small></span></template>
|
||||||
<template #cell-mounted="{ row }"><span class="grid-stack"><strong>{{ row.hwpods.map((item) => item.name).join(', ') || '无' }}</strong><small>{{ row.hwpods.map((item) => item.hwpodId).join(', ') || '未绑定 spec' }}</small></span></template>
|
<template #cell-mounted="{ row }"><span class="grid-stack"><strong>{{ row.hwpods.map((item) => item.name).join(', ') || '无' }}</strong><small>{{ row.hwpods.map((item) => item.hwpodId).join(', ') || '未绑定 spec' }}</small></span></template>
|
||||||
<template #cell-readiness="{ row }"><span class="grid-stack"><strong>{{ row.readiness.filter((stage: HwpodNodeReadinessStage) => stage.status === 'ready').length }}/{{ row.readiness.length }} ready</strong><small>{{ row.readiness.find((stage: HwpodNodeReadinessStage) => stage.status !== 'ready')?.label || '全部阶段 ready' }}</small></span></template>
|
<template #cell-readiness="{ row }"><span class="grid-stack"><strong>{{ row.readiness.filter((stage: HwpodNodeReadinessStage) => stage.status === 'ready').length }}/{{ row.readiness.length }} ready</strong><small>{{ row.readiness.find((stage: HwpodNodeReadinessStage) => stage.status !== 'ready')?.label || '全部阶段 ready' }}</small></span></template>
|
||||||
<template #cell-lastSeen="{ row }">{{ formatTime(row.lastSeenAt) }}</template>
|
<template #cell-lastSeen="{ row }">{{ formatTime(row.lastSeenAt) }}</template>
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{ path: "/admin/billing", name: "AdminBilling", component: () => import("@/views/admin/BillingView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.billing", title: "兑换与订阅", section: "admin" } },
|
{ path: "/admin/billing", name: "AdminBilling", component: () => import("@/views/admin/BillingView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.billing", title: "兑换与订阅", section: "admin" } },
|
||||||
{ path: "/admin/hwpod-groups", redirect: "/hwpods/devices" },
|
{ path: "/admin/hwpod-groups", redirect: "/hwpods/devices" },
|
||||||
{ path: "/hwpods", redirect: "/hwpods/devices" },
|
{ path: "/hwpods", redirect: "/hwpods/devices" },
|
||||||
{ path: "/hwpods/devices/:hwpodId?", name: "HwpodDevices", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD 设备资源", section: "admin" } },
|
{ path: "/hwpods/devices/:hwpodId?", name: "HwpodDevices", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD 设备资源", section: "admin", boundedLayout: true } },
|
||||||
{ path: "/hwpods/nodes/:nodeId?", name: "HwpodNodes", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD Node", section: "admin" } },
|
{ path: "/hwpods/nodes/:nodeId?", name: "HwpodNodes", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD Node", section: "admin", boundedLayout: true } },
|
||||||
{ path: "/hwpods/onboarding/:nodeId?", name: "HwpodOnboarding", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD 节点接入", section: "admin" } },
|
{ path: "/hwpods/onboarding/:nodeId?", name: "HwpodOnboarding", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD 节点接入", section: "admin", boundedLayout: true } },
|
||||||
{ path: "/admin/provider-profiles", name: "ProviderProfiles", component: () => import("@/views/admin/ProviderProfilesView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.providerProfiles", title: "Provider Profiles", section: "admin" } },
|
{ path: "/admin/provider-profiles", name: "ProviderProfiles", component: () => import("@/views/admin/ProviderProfilesView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.providerProfiles", title: "Provider Profiles", section: "admin" } },
|
||||||
{ path: "/admin/provider-profiles/:profile", name: "ProviderProfileDetail", component: () => import("@/views/admin/ProviderProfilesView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.providerProfiles", navParent: "ProviderProfiles", title: "Provider Profile 详情", section: "admin" } },
|
{ path: "/admin/provider-profiles/:profile", name: "ProviderProfileDetail", component: () => import("@/views/admin/ProviderProfilesView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.providerProfiles", navParent: "ProviderProfiles", title: "Provider Profile 详情", section: "admin" } },
|
||||||
{ path: "/skills", name: "Skills", component: () => import("@/views/SkillsView.vue"), meta: { requiresAuth: true, navId: "system.skills", title: "技能包", section: "system" } },
|
{ path: "/skills", name: "Skills", component: () => import("@/views/SkillsView.vue"), meta: { requiresAuth: true, navId: "system.skills", title: "技能包", section: "system" } },
|
||||||
|
|||||||
@@ -389,9 +389,17 @@ export type AgentChatResultResponse = AgentChatResponse & {
|
|||||||
export interface HwpodSpecsResponse { specs?: Array<Record<string, unknown>>; [key: string]: unknown }
|
export interface HwpodSpecsResponse { specs?: Array<Record<string, unknown>>; [key: string]: unknown }
|
||||||
export interface HwpodNodeOpsResponse { ok?: boolean; status?: string; events?: TraceEvent[]; groups?: Array<Record<string, unknown>>; [key: string]: unknown }
|
export interface HwpodNodeOpsResponse { ok?: boolean; status?: string; events?: TraceEvent[]; groups?: Array<Record<string, unknown>>; [key: string]: unknown }
|
||||||
|
|
||||||
export type HwpodResourceStatus = "available" | "busy" | "offline" | "workspace-blocked" | "capability-mismatch" | "unprobed" | "online" | "unconfigured" | string;
|
export type HwpodConnectionStatus = "online" | "offline";
|
||||||
export type HwpodReadinessStatus = "ready" | "blocked" | "unknown" | string;
|
export type HwpodReadinessStatus = "ready" | "blocked" | "unknown" | string;
|
||||||
|
|
||||||
|
export interface HwpodCapabilityCoverage {
|
||||||
|
matched: number;
|
||||||
|
declared: number;
|
||||||
|
actual: number;
|
||||||
|
missing: number;
|
||||||
|
extra: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HwpodBlocker {
|
export interface HwpodBlocker {
|
||||||
code: string;
|
code: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
@@ -431,7 +439,7 @@ export interface HwpodDeviceTopologyItem {
|
|||||||
name: string;
|
name: string;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
status: HwpodResourceStatus;
|
status: HwpodConnectionStatus;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
available: boolean;
|
available: boolean;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
@@ -439,6 +447,7 @@ export interface HwpodDeviceTopologyItem {
|
|||||||
declaredCapabilities: string[];
|
declaredCapabilities: string[];
|
||||||
actualCapabilities: string[];
|
actualCapabilities: string[];
|
||||||
missingCapabilities: string[];
|
missingCapabilities: string[];
|
||||||
|
capabilityCoverage: HwpodCapabilityCoverage;
|
||||||
elements: HwpodElements;
|
elements: HwpodElements;
|
||||||
availability: { ok: boolean | null; status: string; checkedAt: string | null; blocker: HwpodBlocker | null };
|
availability: { ok: boolean | null; status: string; checkedAt: string | null; blocker: HwpodBlocker | null };
|
||||||
operation: HwpodOperationSummary | null;
|
operation: HwpodOperationSummary | null;
|
||||||
@@ -461,7 +470,7 @@ export interface HwpodNodeTopologyItem {
|
|||||||
resourceType: "hwpod-node";
|
resourceType: "hwpod-node";
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: HwpodResourceStatus;
|
status: HwpodConnectionStatus;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
platform: string;
|
platform: string;
|
||||||
version: string | null;
|
version: string | null;
|
||||||
@@ -470,10 +479,11 @@ export interface HwpodNodeTopologyItem {
|
|||||||
lastSeenAt: string | null;
|
lastSeenAt: string | null;
|
||||||
ageMs: number | null;
|
ageMs: number | null;
|
||||||
deviceCount: number;
|
deviceCount: number;
|
||||||
hwpods: Array<{ hwpodId: string; name: string; status: HwpodResourceStatus; busy: boolean; capabilityMismatch: boolean }>;
|
hwpods: Array<{ hwpodId: string; name: string; status: HwpodConnectionStatus; busy: boolean; capabilityMismatch: boolean }>;
|
||||||
declaredCapabilities: string[];
|
declaredCapabilities: string[];
|
||||||
actualCapabilities: string[];
|
actualCapabilities: string[];
|
||||||
missingCapabilities: string[];
|
missingCapabilities: string[];
|
||||||
|
capabilityCoverage: HwpodCapabilityCoverage;
|
||||||
capabilityMismatch: boolean;
|
capabilityMismatch: boolean;
|
||||||
inFlight: { count: number; max: number; busy: boolean };
|
inFlight: { count: number; max: number; busy: boolean };
|
||||||
operation: HwpodOperationSummary | null;
|
operation: HwpodOperationSummary | null;
|
||||||
|
|||||||
@@ -70,18 +70,11 @@ const statusItems = computed(() => [
|
|||||||
const statusFilterOptions = computed(() => tab.value === "nodes"
|
const statusFilterOptions = computed(() => tab.value === "nodes"
|
||||||
? [
|
? [
|
||||||
{ value: "online", label: "在线" },
|
{ value: "online", label: "在线" },
|
||||||
{ value: "busy", label: "执行中" },
|
{ value: "offline", label: "离线" }
|
||||||
{ value: "offline", label: "离线" },
|
|
||||||
{ value: "capability-mismatch", label: "能力不匹配" },
|
|
||||||
{ value: "unconfigured", label: "无 Spec" }
|
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ value: "available", label: "可用" },
|
{ value: "online", label: "在线" },
|
||||||
{ value: "busy", label: "执行中" },
|
{ value: "offline", label: "离线" }
|
||||||
{ value: "offline", label: "离线" },
|
|
||||||
{ value: "capability-mismatch", label: "能力不匹配" },
|
|
||||||
{ value: "workspace-blocked", label: "工作区阻塞" },
|
|
||||||
{ value: "unprobed", label: "待探测" }
|
|
||||||
]);
|
]);
|
||||||
const collectionPage = computed(() => tab.value === "devices" ? hwpod.deviceTopology?.page : tab.value === "nodes" ? hwpod.nodeTopology?.page : null);
|
const collectionPage = computed(() => tab.value === "devices" ? hwpod.deviceTopology?.page : tab.value === "nodes" ? hwpod.nodeTopology?.page : null);
|
||||||
|
|
||||||
@@ -195,7 +188,7 @@ async function copySha(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusLabel(status: string): string {
|
function statusLabel(status: string): string {
|
||||||
return ({ available: "可用", busy: "执行中", offline: "离线", "capability-mismatch": "能力不匹配", "workspace-blocked": "工作区阻塞", unprobed: "待探测", online: "在线", unconfigured: "无 Spec" }[status] ?? status);
|
return ({ online: "在线", offline: "离线" }[status] ?? status);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(value: string | null): string {
|
function formatTime(value: string | null): string {
|
||||||
@@ -310,20 +303,24 @@ function text(value: unknown): string {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.hwpod-console { display: grid; min-width: 0; min-height: 100%; align-content: start; gap: 12px; padding: 8px; color: var(--console-graphite-900); }
|
.hwpod-console { display: grid; height: 100%; min-width: 0; min-height: 0; grid-template-rows: auto auto minmax(0, 1fr); align-content: stretch; gap: 12px; padding: 8px; color: var(--console-graphite-900); overflow: hidden; }
|
||||||
|
.hwpod-console :deep(.console-async-boundary) { display: flex; min-height: 0; flex-direction: column; overflow: hidden; }
|
||||||
|
.hwpod-console :deep(.page-command-filters) { flex-wrap: nowrap; align-items: center; gap: 6px; padding-bottom: 1px; overflow-x: auto; overscroll-behavior-inline: contain; }
|
||||||
.console-tabs { display: flex; gap: 2px; width: fit-content; max-width: 100%; padding: 3px; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-border); overflow-x: auto; }
|
.console-tabs { display: flex; gap: 2px; width: fit-content; max-width: 100%; padding: 3px; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-border); overflow-x: auto; }
|
||||||
.console-tabs button { min-height: 32px; border: 0; border-radius: 5px; background: var(--console-surface-raised); color: var(--console-graphite-600); padding: 0 12px; font-size: 12px; font-weight: 750; white-space: nowrap; cursor: pointer; }
|
.console-tabs button { min-height: 32px; border: 0; border-radius: 5px; background: var(--console-surface-raised); color: var(--console-graphite-600); padding: 0 12px; font-size: 12px; font-weight: 750; white-space: nowrap; cursor: pointer; }
|
||||||
.console-tabs button[data-active="true"] { color: #fff; background: var(--console-graphite-900); }
|
.console-tabs button[data-active="true"] { color: #fff; background: var(--console-graphite-900); }
|
||||||
.console-search { display: flex; min-width: min(420px, 100%); align-items: center; gap: 7px; }
|
.console-search { display: flex; width: min(340px, 100%); min-width: min(340px, 100%); align-items: center; gap: 6px; }
|
||||||
.console-search label, .status-filter { color: var(--console-graphite-600); font-family: var(--console-font-mono); font-size: 10px; font-weight: 760; letter-spacing: 0; text-transform: uppercase; }
|
.console-search label, .status-filter { color: var(--console-graphite-600); font-family: var(--console-font-mono); font-size: 10px; font-weight: 760; letter-spacing: 0; text-transform: uppercase; }
|
||||||
.console-search input { min-width: 0; flex: 1; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 8px 10px; }
|
.console-search input { height: 32px; min-width: 0; flex: 1; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 0 9px; font-size: 11px; }
|
||||||
.console-search button { border: 1px solid var(--console-cyan-700); border-radius: var(--console-radius-sm); background: var(--console-cyan-100); color: var(--console-cyan-700); padding: 7px 10px; font-weight: 750; cursor: pointer; }
|
.console-search button { height: 32px; border: 1px solid var(--console-cyan-700); border-radius: var(--console-radius-sm); background: var(--console-cyan-100); color: var(--console-cyan-700); padding: 0 9px; font-size: 11px; font-weight: 750; cursor: pointer; }
|
||||||
.status-filter { display: flex; align-items: center; gap: 7px; }
|
.status-filter { display: flex; align-items: center; gap: 7px; }
|
||||||
.status-filter select { border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 7px 9px; color: var(--console-graphite-800); }
|
.status-filter select { height: 32px; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 0 8px; color: var(--console-graphite-800); font-size: 11px; }
|
||||||
.probe-button { border: 1px solid var(--console-amber-500); border-radius: var(--console-radius-sm); background: var(--console-amber-100); color: var(--console-amber-700); padding: 7px 10px; font-size: 11px; font-weight: 750; cursor: pointer; }
|
.probe-button { height: 32px; border: 1px solid var(--console-amber-500); border-radius: var(--console-radius-sm); background: var(--console-amber-100); color: var(--console-amber-700); padding: 0 9px; font-size: 11px; font-weight: 750; cursor: pointer; white-space: nowrap; }
|
||||||
|
.hwpod-console :deep(.view-mode-switch) { height: 32px; flex: 0 0 auto; }
|
||||||
|
.hwpod-console :deep(.view-mode-switch button) { height: 28px; min-height: 28px; padding-block: 0; font-size: 11px; }
|
||||||
.spec-warning { display: flex; gap: 8px 20px; align-items: center; margin-bottom: 10px; padding: 10px 13px; border: 1px solid #dfc47d; border-left: 4px solid #aa7a18; border-radius: 7px; background: #fff8df; color: #6b5317; font-size: .78rem; }
|
.spec-warning { display: flex; gap: 8px 20px; align-items: center; margin-bottom: 10px; padding: 10px 13px; border: 1px solid #dfc47d; border-left: 4px solid #aa7a18; border-radius: 7px; background: #fff8df; color: #6b5317; font-size: .78rem; }
|
||||||
.stale-warning { display: flex; gap: 8px 20px; align-items: center; margin-bottom: 10px; padding: 10px 13px; border: 1px solid #e4a8a2; border-left: 4px solid var(--console-red-700); border-radius: var(--console-radius-sm); background: var(--console-red-100); color: var(--console-red-700); font-size: .78rem; }
|
.stale-warning { display: flex; gap: 8px 20px; align-items: center; margin-bottom: 10px; padding: 10px 13px; border: 1px solid #e4a8a2; border-left: 4px solid var(--console-red-700); border-radius: var(--console-radius-sm); background: var(--console-red-100); color: var(--console-red-700); font-size: .78rem; }
|
||||||
.hwpod-workspace { height: clamp(420px, calc(100dvh - 300px), 760px); }
|
.hwpod-workspace { min-height: 0; flex: 1 1 auto; }
|
||||||
.hwpod-workspace :deep(.bounded-workspace-main) { padding: 12px; }
|
.hwpod-workspace :deep(.bounded-workspace-main) { padding: 12px; }
|
||||||
.hwpod-workspace :deep(.resource-stage) { display: grid; height: 100%; min-width: 0; min-height: 0; grid-template-rows: minmax(0, 1fr); }
|
.hwpod-workspace :deep(.resource-stage) { display: grid; height: 100%; min-width: 0; min-height: 0; grid-template-rows: minmax(0, 1fr); }
|
||||||
.hwpod-workspace :deep(.grid-stack) { display: grid; min-width: 0; gap: 3px; }
|
.hwpod-workspace :deep(.grid-stack) { display: grid; min-width: 0; gap: 3px; }
|
||||||
@@ -346,6 +343,6 @@ dd { margin: 3px 0 0; overflow-wrap: anywhere; }
|
|||||||
.collection-pagination div { display: flex; gap: 6px; }
|
.collection-pagination div { display: flex; gap: 6px; }
|
||||||
.collection-pagination button { border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); color: var(--console-graphite-800); padding: 6px 10px; font-weight: 700; cursor: pointer; }
|
.collection-pagination button { border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); color: var(--console-graphite-800); padding: 6px 10px; font-weight: 700; cursor: pointer; }
|
||||||
.collection-pagination button:disabled { opacity: .45; cursor: not-allowed; }
|
.collection-pagination button:disabled { opacity: .45; cursor: not-allowed; }
|
||||||
@media (max-width: 680px) { .hwpod-console { padding: 2px; } .console-tabs { width: 100%; } .console-tabs button { flex: 1 0 auto; } .console-search { min-width: 100%; } .status-filter { width: 100%; justify-content: space-between; } .spec-warning, .stale-warning { align-items: flex-start; flex-direction: column; } .hwpod-workspace { height: clamp(520px, calc(100dvh - 260px), 680px); } .hwpod-workspace :deep(.bounded-workspace-main) { padding: 8px; } }
|
@media (max-width: 680px) { .hwpod-console { padding: 2px; } .console-tabs { width: 100%; } .console-tabs button { flex: 1 0 auto; } .console-search { width: 100%; min-width: 100%; } .status-filter { flex: 0 0 auto; } .spec-warning, .stale-warning { align-items: flex-start; flex-direction: column; } .hwpod-workspace :deep(.bounded-workspace-main) { padding: 8px; } }
|
||||||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; } }
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user