diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 7c3791aa..7b71e9eb 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -246,6 +246,15 @@ function runStaticSmoke() { ] }); + addCheck(checks, blockers, "m3-rendered-workbench-not-m5-fixture", m3RenderedWorkbenchNotM5Fixture(files.app), "Right workbench hardware status and wiring table render the M3 chain, not M5 fixture rows or undefined DOM-node rows.", { + blocker: "runtime_blocker", + evidence: [ + "renderHardwareStatus passes data objects into hardwareGroup", + "renderWiringList uses M3_TRUSTED_ROUTE", + "M5 activeConnections are not mapped into the workbench wiring table" + ] + }); + const help = inspectHelpContract(files); addCheck(checks, blockers, "help-md-contract", help.status, help.summary, { blocker: help.blocker, @@ -558,6 +567,21 @@ function m3FixtureCountsNotDevLive(app) { ); } +function m3RenderedWorkbenchNotM5Fixture(app) { + const hardwareBody = functionBody(app, "renderHardwareStatus"); + const hardwareGroupBody = functionBody(app, "hardwareGroup"); + const wiringBody = functionBody(app, "renderWiringList"); + return ( + /group\.rows\.map\(hardwareRow\)/u.test(hardwareGroupBody) && + !/hardwareRow\(\s*\{/u.test(hardwareBody) && + /M3_TRUSTED_ROUTE\.fromResourceId/u.test(wiringBody) && + /M3_TRUSTED_ROUTE\.patchPanelServiceId/u.test(wiringBody) && + /M3_TRUSTED_ROUTE\.toResourceId/u.test(wiringBody) && + !/activeConnections\.map/u.test(wiringBody) && + !/link\.fromResourceId/u.test(wiringBody) + ); +} + function functionBody(source, functionName) { const match = source.match(new RegExp(`function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u")); if (!match) return ""; diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 33b7f02c..429bf165 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -311,7 +311,7 @@ function renderStaticWorkbench() { renderUnblockList(); renderHardwareStatus(null); renderDrafts(); - renderWiringList(); + renderWiringList(null); renderRecords(null); renderDiagnostics(null); renderGateDiagnostics(); @@ -459,6 +459,7 @@ function renderLiveSurface(live) { : "同源 API 暂不可用,当前仍可使用本地草稿和已加载资源。"; renderHardwareStatus(runtimeSummary); + renderWiringList(runtimeSummary); renderRecords(live); renderDiagnostics(live); renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, reachable ? "live" : "degraded"); @@ -640,7 +641,7 @@ function renderHardwareStatus(summary) { hardwareGroup({ title: "Gateway/Box 在线", rows: [ - hardwareRow({ + { label: "Gateway-SIMU", value: counts ? `${counts.gatewaySessions ?? 0} 会话` : "离线查看", detail: counts @@ -648,8 +649,8 @@ function renderHardwareStatus(summary) { : `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自 SOURCE fixture。`, sourceKind: runtimeCountsSourceKind, tone: runtimeCountsTone - }), - hardwareRow({ + }, + { label: "BOX-SIMU", value: counts ? `${counts.boxResources ?? 0} 资源` : "离线查看", detail: counts @@ -657,66 +658,66 @@ function renderHardwareStatus(summary) { : `${gateSummary.boxResourceCount} 个 BOX 资源来自 SOURCE fixture。`, sourceKind: runtimeCountsSourceKind, tone: runtimeCountsTone - }) + } ] }), hardwareGroup({ title: "BOX-SIMU 端口", rows: [ - hardwareRow({ + { label: "DO1", value: m3Live ? "verified" : "not wired", detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DO1 电平的 live M3 operation/trace/audit/evidence 证据。", sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED", tone: m3Live ? "live" : "blocked" - }), - hardwareRow({ + }, + { label: "DI1", value: m3Live ? "verified" : "not wired", detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DI1 回读的 live M3 operation/trace/audit/evidence 证据。", sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED", tone: m3Live ? "live" : "blocked" - }), - hardwareRow({ + }, + { label: "AI/AO/FREQ", value: "future capability", detail: "当前未声明模拟量/频率闭环,按 future capability 处理。", sourceKind: "SOURCE", tone: "disabled" - }) + } ] }), hardwareGroup({ title: "Patch Panel 连线", rows: [ - hardwareRow({ + { label: "hwlab-patch-panel", value: statusLabel(patchPanel.state), detail: m3Live ? `DEV-LIVE patch-panel live report ${liveM3Evidence.patchPanelLiveReport.reportId ?? liveM3Evidence.patchPanelLiveReport.patchPanelStatusId} 已绑定 M3 route。` - : `${patchPanel.activeConnectionCount} 条 SOURCE 接线;当前 fixture 为 ${describePatchPanelConnections(patchPanel)}。`, + : `${patchPanel.activeConnectionCount} 条 SOURCE fixture 接线;工作台只展示 required M3 route,未升级为 DEV-LIVE。`, sourceKind: m3Live ? "DEV-LIVE" : "SOURCE", tone: patchTone - }), - hardwareRow({ + }, + { label: "DO1 -> patch-panel -> DI1", value: linkStatusLabel(linkTone), detail: m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }), sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED", tone: linkTone - }) + } ] }), hardwareGroup({ title: "写入边界", rows: [ - hardwareRow({ + { label: "硬件控制", value: "已禁用", detail: "Web 不提供硬件写 RPC;不会新增伪硬件写操作。", sourceKind: "SOURCE", tone: "source" - }) + } ] }) ); @@ -757,13 +758,6 @@ function trustedM3Link(connections) { ); } -function describePatchPanelConnections(patchPanel) { - if (!patchPanel.activeConnections.length) return "无活动连线"; - return patchPanel.activeConnections - .map((link) => `${link.fromResourceId}:${link.fromPort} -> ${link.toResourceId}:${link.toPort}`) - .join(";"); -} - function m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }) { if (hasTrustedM3LiveEvidence(liveM3Evidence)) { return `DEV-LIVE M3:${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort};${m3LiveDetail(liveM3Evidence)}。`; @@ -775,7 +769,7 @@ function m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }) { return "BLOCKED:runtime counts 已返回,但 counts 不是 patch-panel live report,不能证明 M3 DEV-LIVE。"; } if (patchPanel.activeConnectionCount > 0) { - return `SOURCE 仅看到 patch-panel 接线:${describePatchPanelConnections(patchPanel)};未冒充 live。`; + return `SOURCE 仅看到非 M3 patch-panel fixture 接线;required M3 route ${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 仍 BLOCKED,未冒充 live。`; } return "BLOCKED:当前没有 DO1 -> patch-panel -> DI1 的可信链路证据。"; } @@ -885,35 +879,26 @@ function renderDrafts() { replaceChildren(el.controlList, ...rows.map(infoCard)); } -function renderWiringList() { +function renderWiringList(summary = null) { const patchPanel = gateSummary.topology.patchPanel; - const wiringStep = gateSummary.steps.find((step) => step.kind === "wiring"); - const traceEvidence = [wiringStep?.id, patchPanel.patchPanelStatusId].filter(Boolean).join(" / "); - const rows = patchPanel.activeConnections.map((link) => { - const tr = document.createElement("tr"); - tr.append(cell(link.fromResourceId, "mono wrap")); - tr.append(cell(link.fromPort, "mono")); - tr.append(cell(patchPanel.serviceId, "mono wrap")); - tr.append(cell(link.toResourceId, "mono wrap")); - tr.append(cell(link.toPort, "mono")); - const status = document.createElement("td"); - status.append(badge(statusLabel(patchPanel.state), patchPanel.state)); - tr.append(status); - tr.append(cell(`${patchPanel.environment} / SOURCE`, "wrap")); - tr.append(cell(traceEvidence || "无", "mono wrap")); - return tr; - }); + const liveM3Evidence = trustedM3LiveEvidenceFrom(summary); + const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence); + const traceEvidence = m3Live + ? [liveM3Evidence.operationId, liveM3Evidence.traceId, liveM3Evidence.auditId, liveM3Evidence.evidenceId].join(" / ") + : "m3-route-required / waiting-for-dev-live"; + 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 ? "开发实况 DEV-LIVE" : "阻塞 BLOCKED", m3Live ? "dev-live" : "blocked")); + tr.append(status); + tr.append(cell(m3Live ? "DEV-LIVE" : "SOURCE / required M3 route", "wrap")); + tr.append(cell(traceEvidence, "mono wrap")); - if (rows.length === 0) { - const tr = document.createElement("tr"); - const td = document.createElement("td"); - td.colSpan = 8; - td.textContent = "当前没有接线记录。"; - tr.append(td); - rows.push(tr); - } - - replaceChildren(el.wiringBody, ...rows); + replaceChildren(el.wiringBody, tr); } function renderRecords(live) { diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index a9b5b1b3..941b0f60 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -89,6 +89,8 @@ assert.match(app, /function normalizePort/); assert.match(app, /LIVE_M3_ID_FIELDS/); assert.match(app, /patchPanelLiveReport/); assert.match(app, /runtimeCountsSourceKind = "SOURCE"/); +assert.doesNotMatch(functionBody(app, "renderHardwareStatus"), /hardwareRow\(\s*\{/u); +assert.match(functionBody(app, "hardwareGroup"), /group\.rows\.map\(hardwareRow\)/u); assert.doesNotMatch(app, /sourceKind:\s*counts\s*\?\s*"DEV-LIVE"/); assert.doesNotMatch(app, /counts\s*&&\s*(?:liveLink|sourceLink|m3Link)[\s\S]{0,80}\?\s*"DEV-LIVE"/); for (const hardwareTerm of [ @@ -197,6 +199,10 @@ assert.match(html, /id="unblock-list"/); assert.match(app, /DB live readiness/); assert.match(app, /res_boxsimu_1:DO1 -> res_boxsimu_2:DI1/); assert.match(app, /patch-panel DO1 -> DI1/); +assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.fromResourceId/); +assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.patchPanelServiceId/); +assert.doesNotMatch(functionBody(app, "renderWiringList"), /activeConnections\.map/u); +assert.doesNotMatch(functionBody(app, "renderWiringList"), /link\.fromResourceId/u); assert.match(buildScript, /"help\.md"/); assert.match(buildScript, /"third_party\/marked\/marked\.esm\.js"/); assert.match(buildScript, /"third_party\/marked\/LICENSE"/); @@ -298,3 +304,18 @@ function beforeSection(source, viewId) { const index = source.search(new RegExp(`]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u")); return index === -1 ? source : source.slice(0, index); } + +function functionBody(source, functionName) { + const match = source.match(new RegExp(`function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u")); + if (!match) return ""; + let depth = 0; + for (let index = match.index + match[0].length - 1; index < source.length; index += 1) { + const char = source[index]; + if (char === "{") depth += 1; + if (char === "}") { + depth -= 1; + if (depth === 0) return source.slice(match.index, index + 1); + } + } + return ""; +}