fix: render patch panel wiring as long table
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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。
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user