fix: render patch panel wiring as long table

This commit is contained in:
Code Queue Review
2026-05-23 08:48:46 +00:00
parent 7f22016c90
commit c18c141c23
10 changed files with 334 additions and 71 deletions
+18 -2
View File
@@ -40,6 +40,14 @@ The default workbench follows the #99 VS Code-style structure:
Unavailable or future controls must be disabled with blocker context. The
frontend must not imply generic direct hardware control.
The `panel-wiring` / `hwlab-patch-panel` view must use a two-column long table
instead of an eight-column wide table. The first table row/header names the two
connected devices, for example `res_boxsimu_1` and `res_boxsimu_2`; following
rows map IO ports, for example `DO1` to `DI1`. The patch-panel service,
status, evidence source, and trace/evidence details remain visible in Chinese
summary or row detail text, but they must not become separate horizontal
columns that force the right sidebar to scroll sideways.
## UX Constraints
The #108 constraints are current baseline:
@@ -78,6 +86,7 @@ heavier browser run:
node web/hwlab-cloud-web/scripts/check.mjs
node web/hwlab-cloud-web/scripts/build.mjs
node scripts/dev-cloud-workbench-smoke.mjs --static
node scripts/dev-cloud-workbench-layout-smoke.mjs --static
node scripts/dev-cloud-workbench-smoke.mjs --dom-only --url http://74.48.78.17:16666/
```
@@ -125,14 +134,21 @@ node scripts/dev-cloud-workbench-layout-smoke.mjs --live --url http://74.48.78.1
滚动。该 UI smoke 只能证明布局/可点击性护栏通过,不等于 M3 DEV-LIVE 硬件
闭环通过,也不能替代 #227 的 DO/DI 功能验收。
该脚本同时覆盖 #276 的接线面板:打开右侧 `接线` 标签,断言
`panel-wiring` 是两列长表、表头为 `res_boxsimu_1` / `res_boxsimu_2`、IO
行为 `DO1``DI1`,并记录右侧接线面板 `scrollWidth <= clientWidth`,禁止
内部横向滚动。该证据仍只证明 UI 布局,不等于 M3 trusted green。
## Stable Sources
- [pikasTech/HWLAB#99](https://github.com/pikasTech/HWLAB/issues/99): default
Cloud Workbench direction.
- [pikasTech/HWLAB#273](https://github.com/pikasTech/HWLAB/issues/273):
Playwright layout/overlap smoke guardrail.
- [pikasTech/HWLAB#108](https://github.com/pikasTech/HWLAB/issues/108): no
outer scroll, Chinese UI, internal Markdown help.
- [pikasTech/HWLAB#273](https://github.com/pikasTech/HWLAB/issues/273):
Playwright layout and overlap guardrail.
- [pikasTech/HWLAB#276](https://github.com/pikasTech/HWLAB/issues/276):
patch-panel wiring panel two-column long-table feedback.
- [code-agent-chat-readiness.md](code-agent-chat-readiness.md): Code Agent
chat 同源 readiness 和 provider credential blocker 边界。
- [docs/cloud-web-workbench.md](../cloud-web-workbench.md): detailed frontend
@@ -200,6 +200,17 @@ test("source/default smoke covers #278 sidebar collapse static contract", () =>
assert.equal(report.checks.find((check) => check.id === "feedback-278-sidebar-collapse-contract")?.status, "pass");
});
test("source/default smoke covers #276 two-column wiring long-table contract", () => {
const report = runDevCloudWorkbenchStaticSmoke();
const check = report.checks.find((item) => item.id === "feedback-276-wiring-long-table");
assert.equal(check?.status, "pass");
assert.deepEqual(
check.evidence.filter((item) => ["res_boxsimu_1", "res_boxsimu_2", "DO1 对 DI1"].includes(item)),
["res_boxsimu_1", "res_boxsimu_2", "DO1 对 DI1"]
);
assert.equal(check.evidence.includes("legacy 8-column table forbidden"), true);
});
test("live workbench identity passes only when runtime commit or image tag matches current source", () => {
assert.equal(
classifyLiveDeploymentIdentity(sourceIdentity, {
@@ -503,11 +514,18 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
assert.equal(desktopCollapsed.toggle.text, "展开资源树");
assert.equal(desktopCollapsed.collapsedWidthGain, true);
assert.equal(desktopCollapsed.keyTargetsReachable, true);
assert.equal(desktopCollapsed.wiring.longTableOk, true);
assert.equal(desktopCollapsed.wiring.noHorizontalScroll, true);
assert.equal(desktopCollapsed.wiring.headers.length, 2);
assert.deepEqual(desktopCollapsed.wiring.legacyHeaderHits, []);
assert.equal(desktopCollapsed.wiring.horizontalScroll.panelScrollWidth <= desktopCollapsed.wiring.horizontalScroll.panelClientWidth + 2, true);
const mobileCollapsed = report.checks.find((check) => check.id === "layout-mobile-collapsed")?.observations;
assert.equal(mobileCollapsed.explorerCollapsed, true);
assert.equal(mobileCollapsed.defaultCollapsedMobile, true);
assert.equal(mobileCollapsed.keyTargetsReachable, true);
assert.equal(mobileCollapsed.wiring.longTableOk, true);
assert.equal(mobileCollapsed.wiring.noHorizontalScroll, true);
const desktopExpanded = report.checks.find((check) => check.id === "layout-desktop-expanded")?.observations;
assert.equal(desktopExpanded.failures.length, 0);
+150 -19
View File
@@ -102,7 +102,8 @@ const helpRouteAliases = Object.freeze(["/help"]);
const incompletePrimaryNavLabels = Object.freeze(["台", "证", "诊", "帮"]);
const requiredWiringColumns = Object.freeze([
const requiredWiringLongTableHeaders = Object.freeze(["res_boxsimu_1", "res_boxsimu_2"]);
const forbiddenLegacyWiringColumns = Object.freeze([
"源设备",
"源端口",
"接线盘",
@@ -112,6 +113,7 @@ const requiredWiringColumns = Object.freeze([
"证据来源",
"轨迹/证据"
]);
const requiredWiringSummaryTerms = Object.freeze(["hwlab-patch-panel", "状态", "证据来源", "轨迹/证据"]);
const requiredTrustedRecordTerms = Object.freeze([
"Code Agent 对话记录",
@@ -367,9 +369,15 @@ function runStaticSmoke() {
evidence: [...helpRouteAliases, "routeFromLocation final fallback is workspace"]
});
addCheck(checks, blockers, "feedback-120-wiring-table", wiringTableContract(files.html), "Patch-panel wiring panel is a table with source, target, status, evidence source, and 轨迹/证据 columns.", {
addCheck(checks, blockers, "feedback-276-wiring-long-table", wiringTableContract(files.html, files.styles, files.app), "Patch-panel wiring panel is a two-column long table: device headers first, IO mapping rows after, and metadata moved to Chinese summary/detail text.", {
blocker: "contract_blocker",
evidence: requiredWiringColumns
evidence: [
...requiredWiringLongTableHeaders,
"DO1 对 DI1",
"data-wiring-layout=\"two-column-long-table\"",
"legacy 8-column table forbidden",
...requiredWiringSummaryTerms
]
});
addCheck(checks, blockers, "trusted-record-groups", trustedRecordGroups(files), "Trusted Records panel groups Code Agent, wiring/operation, audit, and evidence summaries with source labels and live-failure fallback.", {
@@ -1032,9 +1040,9 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
evidenceLevel: useLiveUrl ? blockers.length === 0 ? "DEV-LIVE-LAYOUT" : "BLOCKED" : "SOURCE",
devLive: false,
summary: useLiveUrl
? "Live browser layout smoke verifies workbench layout, hit targets, overflow, and /gate usability only; it is not M3 hardware acceptance."
: "Static local browser layout smoke verifies workbench layout, hit targets, overflow, and /gate usability only; deployment still requires DEV live verification.",
refs: ["pikasTech/HWLAB#273", "pikasTech/HWLAB#278", "pikasTech/HWLAB#287", "pikasTech/HWLAB#288", "pikasTech/HWLAB#99", "pikasTech/HWLAB#227"],
? "Live browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the two-column wiring panel only; it is not M3 hardware acceptance."
: "Static local browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the two-column wiring panel only; deployment still requires DEV live verification.",
refs: ["pikasTech/HWLAB#273", "pikasTech/HWLAB#276", "pikasTech/HWLAB#278", "pikasTech/HWLAB#287", "pikasTech/HWLAB#288", "pikasTech/HWLAB#99", "pikasTech/HWLAB#227"],
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks,
blockers,
@@ -1192,7 +1200,8 @@ function baseReport({
"pikasTech/HWLAB#131",
"pikasTech/HWLAB#143",
"pikasTech/HWLAB#164",
"pikasTech/HWLAB#224"
"pikasTech/HWLAB#224",
"pikasTech/HWLAB#276"
],
mode,
url,
@@ -1609,8 +1618,7 @@ function defaultWorkspaceIsSanitized({ html, app }) {
functionBody(app, "codeAgentControlSummary"),
functionBody(app, "codeAgentBlockedSummary"),
functionBody(app, "codeAgentAvailabilitySummary"),
functionBody(app, "failureMessage"),
functionBody(app, "renderWiringList")
functionBody(app, "failureMessage")
].join("\n");
return (
defaultHomepageForbiddenTerms.every((term) => !textHasForbiddenTerm(defaultShellText, term)) &&
@@ -1694,9 +1702,31 @@ function helpRouteAliasesAreServed(app, artifactPublisher, buildScript, distCont
);
}
function wiringTableContract(html) {
function wiringTableContract(html, styles = "", app = "") {
const wiringPanel = html.match(/<section\b[^>]*\bdata-side-panel=["']wiring["'][\s\S]*?<\/section>/u)?.[0] ?? "";
return requiredWiringColumns.every((column) => wiringPanel.includes(`<th>${column}</th>`));
const headerCount = (wiringPanel.match(/<th\b/gu) ?? []).length;
const bodyRow = wiringPanel.match(/<tr\b[^>]*\bdata-wiring-row=["']io["'][\s\S]*?<\/tr>/u)?.[0] ?? "";
const bodyCellCount = (bodyRow.match(/<td\b/gu) ?? []).length;
const renderWiringBody = functionBody(app, "renderWiringList");
return (
/data-wiring-layout=["']two-column-long-table["']/u.test(wiringPanel) &&
/class=["'][^"']*\bwiring-long-table\b/u.test(wiringPanel) &&
headerCount === 2 &&
bodyCellCount === 2 &&
requiredWiringLongTableHeaders.every((header) => wiringPanel.includes(header)) &&
["DO1", "DI1"].every((port) => wiringPanel.includes(port)) &&
requiredWiringSummaryTerms.every((term) => wiringPanel.includes(term)) &&
forbiddenLegacyWiringColumns.every((column) => !wiringPanel.includes(`<th>${column}</th>`)) &&
/id=["']wiring-summary["']/u.test(wiringPanel) &&
/overflow-x:\s*hidden/u.test(styles.match(/\.wiring-table-wrap\s*\{[\s\S]*?\}/u)?.[0] ?? "") &&
!/\.wiring-table\s*\{[\s\S]*?min-width:\s*9\d\dpx/u.test(styles) &&
!/min-width:\s*980px/u.test(styles) &&
/table-layout:\s*fixed/u.test(styles.match(/\.wiring-long-table\s*\{[\s\S]*?\}/u)?.[0] ?? "") &&
/M3_TRUSTED_ROUTE\.fromResourceId/u.test(renderWiringBody) &&
/M3_TRUSTED_ROUTE\.toResourceId/u.test(renderWiringBody) &&
/M3_TRUSTED_ROUTE\.patchPanelServiceId/u.test(renderWiringBody) &&
!/tr\.append\(cell\(M3_TRUSTED_ROUTE\.fromResourceId/u.test(renderWiringBody)
);
}
function hardwareStatusStructureContract({ html, app, styles }) {
@@ -2488,10 +2518,20 @@ async function inspectLiveDom(url, options = {}) {
),
defaultBackstageHidden: !/(Gate|诊断|验收|BLOCKED|M0-M5|执行轨迹)/u.test(document.querySelector(".center-workspace")?.innerText ?? ""),
gateRouteAvailable: [...document.querySelectorAll("[data-route='gate']")].some((button) => button.textContent?.trim() === "内部复核"),
wiringHeaders: [...document.querySelectorAll(".wiring-table thead th")].map((cell) => cell.textContent?.trim() ?? ""),
wiringLayout: document.querySelector(".wiring-table-wrap")?.getAttribute("data-wiring-layout") ?? "",
wiringHeaderDevices: [
document.querySelector("#wiring-source-device")?.textContent?.trim() ?? "",
document.querySelector("#wiring-target-device")?.textContent?.trim() ?? ""
],
wiringHeaderCount: document.querySelectorAll(".wiring-table thead th").length,
legacyWiringHeaders: [...document.querySelectorAll(".wiring-table thead th")].map((cell) => cell.textContent?.trim() ?? "").filter((text) =>
["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据"].includes(text)
),
m3WiringRows: [...document.querySelectorAll("#wiring-body tr")].map((row) =>
[...row.querySelectorAll("td")].map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? "")
),
wiringBodyCellCounts: [...document.querySelectorAll("#wiring-body tr")].map((row) => row.querySelectorAll("td").length),
wiringSummaryText: document.querySelector("#wiring-summary")?.textContent?.replace(/\s+/gu, " ").trim() ?? "",
coreControlsVisible: {
commandInput: visible("#command-input"),
commandSend: visible("#command-send"),
@@ -2520,14 +2560,20 @@ async function inspectLiveDom(url, options = {}) {
dom.navLabelsPresent &&
dom.defaultBackstageHidden &&
dom.gateRouteAvailable &&
requiredWiringColumns.every((column) => dom.wiringHeaders.includes(column)) &&
dom.wiringLayout === "two-column-long-table" &&
dom.wiringHeaderCount === 2 &&
dom.legacyWiringHeaders.length === 0 &&
requiredWiringLongTableHeaders.every((header) => dom.wiringHeaderDevices.includes(header)) &&
dom.wiringBodyCellCounts.every((count) => count === 2) &&
dom.m3WiringRows.some((row) =>
row.includes("res_boxsimu_1") &&
row.includes("DO1") &&
row.includes("hwlab-patch-panel") &&
row.includes("res_boxsimu_2") &&
row.includes("DI1")
row.length === 2 &&
row[0].includes("DO1") &&
row[0].includes("hwlab-patch-panel") &&
row[1].includes("DI1") &&
row[1].includes("证据来源") &&
row[1].includes("轨迹/证据")
) &&
requiredWiringSummaryTerms.every((term) => dom.wiringSummaryText.includes(term)) &&
(postGuard ? postGuard.attempts.length === 0 : true) &&
Object.values(dom.coreControlsVisible).every(Boolean);
return {
@@ -2539,6 +2585,9 @@ async function inspectLiveDom(url, options = {}) {
`htmlOverflow=${dom.htmlOverflow}`,
`firstViewportForbidden=${dom.firstViewportForbiddenPresent.join(",") || "none"}`,
`gateRouteAvailable=${dom.gateRouteAvailable}`,
`wiringLayout=${dom.wiringLayout}`,
`wiringHeaderDevices=${dom.wiringHeaderDevices.join("|")}`,
`wiringBodyCellCounts=${dom.wiringBodyCellCounts.join("|")}`,
`m3WiringRows=${dom.m3WiringRows.length}`,
...(postGuard ? [`codeAgentPostAttempts=${postGuard.attempts.length}`] : [])
],
@@ -4004,6 +4053,7 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
[".hardware-status", ".side-workspace", "hardware status vs side workspace"],
["#command-form", ".right-sidebar", "Code Agent command form vs right sidebar"]
];
const wiringLegacyHeaders = ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据"];
const drawerSelectorLabels = [
[".quick-actions button", "资源树快捷操作"],
["#resource-tree summary", "资源树分组"],
@@ -4309,6 +4359,75 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
const overflowOk = overflowChecks.every((check) => check.ok);
const sidePanelScroll = await scrollableProbe("#panel-control");
const sidePanelOverflowUsable = sidePanelScroll.exists && (sidePanelScroll.scrolls || sidePanelScroll.overflowY !== "hidden");
const wiringTab = document.querySelector("#tab-wiring");
if (wiringTab?.getAttribute("aria-selected") !== "true") {
wiringTab?.click();
await new Promise((resolve) => requestAnimationFrame(resolve));
}
const wiringPanel = document.querySelector("#panel-wiring");
const wiringWrap = document.querySelector(".wiring-table-wrap");
const wiringTable = document.querySelector(".wiring-long-table");
const wiringHeaders = [...document.querySelectorAll(".wiring-table thead th")].map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? "");
const wiringRows = [...document.querySelectorAll("#wiring-body tr")].map((row) =>
[...row.querySelectorAll("td")].map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? "")
);
const wiringLegacyHeaderHits = wiringHeaders.filter((header) => wiringLegacyHeaders.includes(header));
const wiringHorizontalScroll = {
panelClientWidth: wiringPanel?.clientWidth ?? 0,
panelScrollWidth: wiringPanel?.scrollWidth ?? 0,
wrapClientWidth: wiringWrap?.clientWidth ?? 0,
wrapScrollWidth: wiringWrap?.scrollWidth ?? 0,
tableClientWidth: wiringTable?.clientWidth ?? 0,
tableScrollWidth: wiringTable?.scrollWidth ?? 0,
panelOverflowX: wiringPanel ? getComputedStyle(wiringPanel).overflowX : "missing",
wrapOverflowX: wiringWrap ? getComputedStyle(wiringWrap).overflowX : "missing"
};
const wiringNoHorizontalScroll =
Boolean(wiringPanel && wiringWrap && wiringTable) &&
wiringHorizontalScroll.panelScrollWidth <= wiringHorizontalScroll.panelClientWidth + 2 &&
wiringHorizontalScroll.wrapScrollWidth <= wiringHorizontalScroll.wrapClientWidth + 2;
const wiringLongTableOk =
wiringWrap?.getAttribute("data-wiring-layout") === "two-column-long-table" &&
wiringHeaders.length === 2 &&
wiringHeaders.includes("res_boxsimu_1 互连设备 A") &&
wiringHeaders.includes("res_boxsimu_2 互连设备 B") &&
wiringLegacyHeaderHits.length === 0 &&
wiringRows.some((row) =>
row.length === 2 &&
row[0].includes("DO1") &&
row[0].includes("hwlab-patch-panel") &&
row[1].includes("DI1") &&
row[1].includes("证据来源") &&
row[1].includes("轨迹/证据")
);
if (!wiringLongTableOk) {
failures.push({
failureType: "blocked/skip",
selector: "#panel-wiring .wiring-long-table",
viewport,
summary: "#276 wiring panel is not the required two-column long table with device headers and DO1/DI1 row details.",
wiring: {
headers: wiringHeaders,
legacyHeaderHits: wiringLegacyHeaderHits,
rows: wiringRows,
layout: wiringWrap?.getAttribute("data-wiring-layout") ?? null
}
});
}
if (!wiringNoHorizontalScroll) {
failures.push({
failureType: "overflow",
selector: "#panel-wiring",
viewport,
summary: "#276 wiring panel exposes internal horizontal scroll or clipped table width.",
wiringHorizontalScroll
});
}
const controlTab = document.querySelector("#tab-control");
if (controlTab?.getAttribute("aria-selected") !== "true") {
controlTab?.click();
await new Promise((resolve) => requestAnimationFrame(resolve));
}
const toggle = {
text: toggleElement?.textContent?.replace(/\s+/gu, " ").trim() ?? "",
ariaLabel: toggleElement?.getAttribute("aria-label") ?? "",
@@ -4469,10 +4588,14 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
drawerReachable &&
defaultCollapsedMobile &&
sidePanelOverflowUsable;
const passWithWiring =
pass &&
wiringLongTableOk &&
wiringNoHorizontalScroll;
return {
mode,
viewport,
pass,
pass: passWithWiring,
explorerCollapsed,
explorerVisible,
toggle,
@@ -4492,6 +4615,14 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
blockedDrawerTargets,
sidePanelScroll,
sidePanelOverflowUsable,
wiring: {
longTableOk: wiringLongTableOk,
noHorizontalScroll: wiringNoHorizontalScroll,
headers: wiringHeaders,
legacyHeaderHits: wiringLegacyHeaderHits,
rows: wiringRows,
horizontalScroll: wiringHorizontalScroll
},
semanticOverlapChecks,
overflowChecks,
defaultCollapsedMobile,
+20 -8
View File
@@ -2053,22 +2053,34 @@ function assertDevCloudWorkbenchDomObservations(dom, label) {
assert.equal(dom.codeAgentPostGuard.blockedPath, "/v1/agent/chat", `${label}.codeAgentPostGuard.blockedPath`);
assert.equal(dom.codeAgentPostGuard.attemptedCount, 0, `${label}.codeAgentPostGuard.attemptedCount`);
}
assertStringArray(dom.wiringHeaders, `${label}.wiringHeaders`, { minLength: 8 });
for (const column of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "trace/evidence"]) {
assert.ok(dom.wiringHeaders.includes(column), `${label}.wiringHeaders missing ${column}`);
assert.equal(dom.wiringLayout, "two-column-long-table", `${label}.wiringLayout`);
assertStringArray(dom.wiringHeaderDevices, `${label}.wiringHeaderDevices`, { minLength: 2 });
assert.equal(dom.wiringHeaderDevices.includes("res_boxsimu_1"), true, `${label}.wiringHeaderDevices missing res_boxsimu_1`);
assert.equal(dom.wiringHeaderDevices.includes("res_boxsimu_2"), true, `${label}.wiringHeaderDevices missing res_boxsimu_2`);
assert.equal(dom.wiringHeaderCount, 2, `${label}.wiringHeaderCount`);
assertStringArray(dom.legacyWiringHeaders, `${label}.legacyWiringHeaders`);
assert.equal(dom.legacyWiringHeaders.length, 0, `${label}.legacyWiringHeaders must not restore legacy wide columns`);
assertArray(dom.wiringBodyCellCounts, `${label}.wiringBodyCellCounts`);
for (const count of dom.wiringBodyCellCounts) {
assert.equal(count, 2, `${label}.wiringBodyCellCounts must stay two-column`);
}
assertArray(dom.m3WiringRows, `${label}.m3WiringRows`);
assert.ok(
dom.m3WiringRows.some((row) =>
Array.isArray(row) &&
row.includes("res_boxsimu_1") &&
row.includes("DO1") &&
row.includes("hwlab-patch-panel") &&
row.includes("res_boxsimu_2") &&
row.includes("DI1")
row.length === 2 &&
row[0].includes("DO1") &&
row[0].includes("hwlab-patch-panel") &&
row[1].includes("DI1") &&
row[1].includes("证据来源") &&
row[1].includes("轨迹/证据")
),
`${label}.m3WiringRows missing required M3 route`
);
assert.match(dom.wiringSummaryText, /hwlab-patch-panel/u, `${label}.wiringSummaryText missing patch-panel`);
assert.match(dom.wiringSummaryText, /状态/u, `${label}.wiringSummaryText missing status`);
assert.match(dom.wiringSummaryText, /证据来源/u, `${label}.wiringSummaryText missing evidence source`);
assert.match(dom.wiringSummaryText, /轨迹\/证据/u, `${label}.wiringSummaryText missing trace/evidence`);
assertObject(dom.coreControlsVisible, `${label}.coreControlsVisible`);
for (const control of ["commandInput", "commandSend", "agentChatStatus", "conversationList", "hardwareList", "wiringTab", "recordsTab"]) {
assert.equal(dom.coreControlsVisible[control], true, `${label}.coreControlsVisible.${control}`);
+32 -12
View File
@@ -868,7 +868,7 @@ function renderCapabilityList() {
},
{
title: "接线核对",
detail: "在右侧接线表核对源端口、接线盘、目标端口与证据记录。",
detail: "在右侧两列接线表核对互连设备、DO/DI 对应关系与证据记录。",
tone: "source"
}
];
@@ -899,7 +899,7 @@ function renderTaskList() {
},
{
title: "2. 核对接线",
detail: "打开右侧“接线”表,确认源设备、接线盘、目标设备和轨迹/证据是否匹配。",
detail: "打开右侧“接线”表,确认 res_boxsimu_1 与 res_boxsimu_2 的 DO/DI 对应关系,以及接线盘和轨迹/证据摘要。",
tone: "source"
},
{
@@ -1390,19 +1390,39 @@ function renderWiringList(summary = null) {
const traceEvidence = m3Live
? [liveM3Evidence.operationId, liveM3Evidence.traceId, liveM3Evidence.auditId, liveM3Evidence.evidenceId].join(" / ")
: "等待可信记录";
const stateLabel = m3Live ? "实况已验证" : "待可信记录";
const sourceLabel = m3Live ? "实况可信记录" : "来源拓扑";
const sourceHeader = byId("wiring-source-device");
const targetHeader = byId("wiring-target-device");
if (sourceHeader) sourceHeader.textContent = M3_TRUSTED_ROUTE.fromResourceId;
if (targetHeader) targetHeader.textContent = M3_TRUSTED_ROUTE.toResourceId;
const tr = document.createElement("tr");
tr.append(cell(M3_TRUSTED_ROUTE.fromResourceId, "mono wrap"));
tr.append(cell(M3_TRUSTED_ROUTE.fromPort, "mono"));
tr.append(cell(M3_TRUSTED_ROUTE.patchPanelServiceId, "mono wrap"));
tr.append(cell(M3_TRUSTED_ROUTE.toResourceId, "mono wrap"));
tr.append(cell(M3_TRUSTED_ROUTE.toPort, "mono"));
const status = document.createElement("td");
status.append(badge(m3Live ? "实况已验证" : "待可信记录", m3Live ? "dev-live" : "blocked"));
tr.append(status);
tr.append(cell(m3Live ? "实况可信记录" : "来源拓扑", "wrap"));
tr.append(cell(traceEvidence, "mono wrap"));
tr.dataset.sourceKind = m3Live ? "DEV-LIVE" : "SOURCE";
tr.dataset.wiringRow = "io";
const sourceCell = cell("", "");
sourceCell.append(textSpan(M3_TRUSTED_ROUTE.fromPort, "wiring-port mono"));
sourceCell.append(textSpan(`源端口;经 ${M3_TRUSTED_ROUTE.patchPanelServiceId};状态:${stateLabel}`, "wiring-detail"));
const targetCell = cell("", "");
targetCell.append(textSpan(M3_TRUSTED_ROUTE.toPort, "wiring-port mono"));
targetCell.append(textSpan(`目标端口;证据来源:${sourceLabel};轨迹/证据:${traceEvidence}`, "wiring-detail mono wrap"));
tr.append(sourceCell, targetCell);
replaceChildren(el.wiringBody, tr);
const summaryElement = byId("wiring-summary");
if (summaryElement) {
replaceChildren(
summaryElement,
document.createTextNode(`接线盘:${M3_TRUSTED_ROUTE.patchPanelServiceId};状态:`),
badge(stateLabel, m3Live ? "dev-live" : "blocked"),
document.createTextNode(`;证据来源:${sourceLabel};轨迹/证据:${traceEvidence}`)
);
}
}
function renderRecords(live) {
+1 -1
View File
@@ -18,7 +18,7 @@
## 右侧硬件与状态面板
- BOX-SIMU / Gateway-SIMU / Patch Panel 卡片展示只读计数、source fallback 与硬件写入边界。
- 接线:`hwlab-patch-panel` 面板使用表格展示源设备、源端口、接线盘、目标设备、目标端口、状态、证据来源和轨迹/证据
- 接线:`hwlab-patch-panel` 面板使用两列多行长表展示互连设备,首行是 `res_boxsimu_1``res_boxsimu_2`,后续行是 `DO1``DI1`;状态、证据来源和轨迹/证据放在中文摘要与行内说明中,不作为横向宽列展开
- M3 虚拟硬件可信闭环的上位约束是:`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1`。只有观察到这条完整链路并带有可信记录时,才可作为 M3 DEV-LIVE 依据。
- Web 前端不会新增 `hardware.*` 写 RPC、patch-panel 写接口、audit 写接口或 evidence 写接口;M3 控制的 operation/audit/evidence 字段由 `/v1/m3/io` 响应返回,runtime durable 未 green 时必须显示 BLOCKED。
+23 -22
View File
@@ -142,8 +142,8 @@
<h2>DEV 健康契约</h2>
<span class="state-tag tone-source">来源 SOURCE fixture</span>
</div>
<div class="table-wrap">
<table>
<div class="table-wrap wide-table-wrap">
<table class="wide-table">
<thead>
<tr>
<th>组件</th>
@@ -299,34 +299,35 @@
<h2>接线:hwlab-patch-panel</h2>
<span class="state-tag tone-source">来源拓扑接线</span>
</div>
<div class="table-wrap wiring-table-wrap">
<table class="wiring-table">
<div class="table-wrap wiring-table-wrap" data-wiring-layout="two-column-long-table">
<table class="wiring-table wiring-long-table" aria-describedby="wiring-summary">
<thead>
<tr>
<th>源设备</th>
<th>源端口</th>
<th>接线盘</th>
<th>目标设备</th>
<th>目标端口</th>
<th>状态</th>
<th>证据来源</th>
<th>轨迹/证据</th>
<tr data-wiring-row="devices">
<th scope="col">
<span class="mono wrap" id="wiring-source-device">res_boxsimu_1</span>
<small>互连设备 A</small>
</th>
<th scope="col">
<span class="mono wrap" id="wiring-target-device">res_boxsimu_2</span>
<small>互连设备 B</small>
</th>
</tr>
</thead>
<tbody id="wiring-body">
<tr data-source-kind="SOURCE">
<td class="mono wrap">res_boxsimu_1</td>
<td class="mono">DO1</td>
<td class="mono wrap">hwlab-patch-panel</td>
<td class="mono wrap">res_boxsimu_2</td>
<td class="mono">DI1</td>
<td><span class="state-tag tone-blocked">待可信记录</span></td>
<td class="wrap">来源拓扑</td>
<td class="mono wrap">等待可信记录</td>
<tr data-source-kind="SOURCE" data-wiring-row="io">
<td>
<span class="wiring-port mono">DO1</span>
<small class="wiring-detail">源端口;经 hwlab-patch-panel;状态:待可信记录</small>
</td>
<td>
<span class="wiring-port mono">DI1</span>
<small class="wiring-detail">目标端口;证据来源:来源拓扑;轨迹/证据:等待可信记录</small>
</td>
</tr>
</tbody>
</table>
</div>
<p class="wiring-summary" id="wiring-summary">接线盘:hwlab-patch-panel;状态:待可信记录;证据来源:来源拓扑;轨迹/证据:等待可信记录。</p>
</section>
<section class="side-panel" id="panel-records" role="tabpanel" aria-labelledby="tab-records" data-side-panel="records" hidden>
+9 -2
View File
@@ -304,8 +304,15 @@ for (const oneCharNavLabel of [">台<", ">证<", ">诊<", ">帮<"]) {
assert.doesNotMatch(html, new RegExp(escapeRegExp(oneCharNavLabel)), `legacy one-character nav label must be removed: ${oneCharNavLabel}`);
}
assert.doesNotMatch(html, /data-side-tab-jump="records"/u, "trusted records must not remain a left-side standalone shortcut");
for (const wiringHeader of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据"]) {
assert.match(html, new RegExp(`<th>${escapeRegExp(wiringHeader)}<\\/th>`), `missing wiring column ${wiringHeader}`);
const wiringPanel = html.match(/<section\b[^>]*\bid="panel-wiring"[\s\S]*?<\/section>/u)?.[0] ?? "";
assert.match(wiringPanel, /data-wiring-layout="two-column-long-table"/u, "wiring panel must use the #276 two-column long-table layout");
assert.equal((wiringPanel.match(/<th\b/gu) ?? []).length, 2, "wiring table must have exactly two device header columns");
assert.equal((wiringPanel.match(/<td\b/gu) ?? []).length, 2, "wiring table static IO row must have exactly two port cells");
for (const wiringTerm of ["res_boxsimu_1", "res_boxsimu_2", "DO1", "DI1", "hwlab-patch-panel", "状态", "证据来源", "轨迹/证据"]) {
assert.match(wiringPanel, new RegExp(escapeRegExp(wiringTerm)), `missing wiring term ${wiringTerm}`);
}
for (const legacyWideHeader of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据"]) {
assert.doesNotMatch(wiringPanel, new RegExp(`<th>${escapeRegExp(legacyWideHeader)}<\\/th>`), `legacy wide wiring column must not return: ${legacyWideHeader}`);
}
assert.match(html, /id="unblock-list"/);
assert.match(app, /DB live readiness/);
@@ -89,8 +89,11 @@ function assertControlPanelDom(html) {
for (const value of ["res_boxsimu_1", "DO1", "hwlab-patch-panel", "res_boxsimu_2", "DI1"]) {
assert.match(wiringPanel, new RegExp(escapeRegExp(value), "u"), `wiring panel missing ${value}`);
}
assert.match(wiringPanel, /data-wiring-layout="two-column-long-table"/u, "wiring panel must use the #276 two-column long-table layout");
assert.equal((wiringPanel.match(/<th\b/gu) ?? []).length, 2, "wiring table must have exactly two device columns");
assert.equal((wiringPanel.match(/<td\b/gu) ?? []).length, 2, "static wiring IO row must have exactly two port cells");
for (const header of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据"]) {
assert.match(wiringPanel, new RegExp(`<th>${escapeRegExp(header)}<\\/th>`, "u"), `wiring table missing ${header}`);
assert.doesNotMatch(wiringPanel, new RegExp(`<th>${escapeRegExp(header)}<\\/th>`, "u"), `wiring table must not restore legacy wide column ${header}`);
}
assert.match(wiringPanel, /待可信记录/u, "static wiring row must stay blocked");
assert.match(wiringPanel, /等待可信记录/u, "static wiring row must require live evidence");
+59 -4
View File
@@ -1066,19 +1066,74 @@ h3 {
}
.table-wrap {
overflow: auto;
border: 1px solid var(--line);
overflow-x: auto;
overflow-y: auto;
}
table {
width: 100%;
min-width: 860px;
border-collapse: collapse;
background: var(--surface-2);
}
.wiring-table {
min-width: 980px;
.wide-table {
min-width: 860px;
}
.wiring-table-wrap {
width: 100%;
min-width: 0;
overflow-x: hidden;
overflow-y: visible;
}
.wiring-long-table {
table-layout: fixed;
min-width: 0;
}
.wiring-long-table th,
.wiring-long-table td {
width: 50%;
}
.wiring-long-table th {
color: var(--text);
font-size: 11px;
text-transform: none;
}
.wiring-long-table th small,
.wiring-detail {
display: block;
margin-top: 4px;
color: var(--muted);
font-family: var(--font);
font-size: 10px;
font-weight: 640;
line-height: 1.35;
text-transform: none;
}
.wiring-port {
display: block;
color: var(--text);
font-size: 13px;
font-weight: 840;
line-height: 1.15;
}
.wiring-summary {
min-width: 0;
margin: 0;
padding: 9px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
font-size: 11px;
line-height: 1.45;
overflow-wrap: anywhere;
}
th,