From c18c141c2390320acd2eb16ef5e59c8310af8d44 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sat, 23 May 2026 08:48:46 +0000 Subject: [PATCH] fix: render patch panel wiring as long table --- docs/reference/cloud-workbench.md | 20 ++- scripts/dev-cloud-workbench-smoke.test.mjs | 18 ++ scripts/src/dev-cloud-workbench-smoke-lib.mjs | 169 ++++++++++++++++-- scripts/validate-dev-gate-report.mjs | 28 ++- web/hwlab-cloud-web/app.mjs | 44 +++-- web/hwlab-cloud-web/help.md | 2 +- web/hwlab-cloud-web/index.html | 45 ++--- web/hwlab-cloud-web/scripts/check.mjs | 11 +- .../scripts/m3-control-panel-guard.mjs | 5 +- web/hwlab-cloud-web/styles.css | 63 ++++++- 10 files changed, 334 insertions(+), 71 deletions(-) diff --git a/docs/reference/cloud-workbench.md b/docs/reference/cloud-workbench.md index e22bf389..f6b2488d 100644 --- a/docs/reference/cloud-workbench.md +++ b/docs/reference/cloud-workbench.md @@ -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 diff --git a/scripts/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs index 9c0c9d6d..04e57a30 100644 --- a/scripts/dev-cloud-workbench-smoke.test.mjs +++ b/scripts/dev-cloud-workbench-smoke.test.mjs @@ -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); diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 2469d8b4..78d34a5f 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -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(/]*\bdata-side-panel=["']wiring["'][\s\S]*?<\/section>/u)?.[0] ?? ""; - return requiredWiringColumns.every((column) => wiringPanel.includes(`${column}`)); + const headerCount = (wiringPanel.match(/]*\bdata-wiring-row=["']io["'][\s\S]*?<\/tr>/u)?.[0] ?? ""; + const bodyCellCount = (bodyRow.match(/ wiringPanel.includes(header)) && + ["DO1", "DI1"].every((port) => wiringPanel.includes(port)) && + requiredWiringSummaryTerms.every((term) => wiringPanel.includes(term)) && + forbiddenLegacyWiringColumns.every((column) => !wiringPanel.includes(`${column}`)) && + /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, diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index 9e57d10c..0eb79616 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -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}`); diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index da7e48a4..3fefdd5b 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -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) { diff --git a/web/hwlab-cloud-web/help.md b/web/hwlab-cloud-web/help.md index bfa0465b..af76b099 100644 --- a/web/hwlab-cloud-web/help.md +++ b/web/hwlab-cloud-web/help.md @@ -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。 diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 0daccd5e..574c6187 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -142,8 +142,8 @@

DEV 健康契约

来源 SOURCE fixture -
- +
+
@@ -299,34 +299,35 @@

接线:hwlab-patch-panel

来源拓扑接线 -
-
组件
+
+
- - - - - - - - - + + + - - - - - - - - - + + +
源设备源端口接线盘目标设备目标端口状态证据来源轨迹/证据
+ res_boxsimu_1 + 互连设备 A + + res_boxsimu_2 + 互连设备 B +
res_boxsimu_1DO1hwlab-patch-panelres_boxsimu_2DI1待可信记录来源拓扑等待可信记录
+ DO1 + 源端口;经 hwlab-patch-panel;状态:待可信记录 + + DI1 + 目标端口;证据来源:来源拓扑;轨迹/证据:等待可信记录 +
+

接线盘:hwlab-patch-panel;状态:待可信记录;证据来源:来源拓扑;轨迹/证据:等待可信记录。