feat: enrich cloud workbench hardware status
feat: enrich cloud workbench hardware status
This commit is contained in:
@@ -73,6 +73,21 @@ const requiredWiringColumns = Object.freeze([
|
||||
"trace/evidence"
|
||||
]);
|
||||
|
||||
const requiredHardwareStatusTerms = Object.freeze([
|
||||
"hardware-source-status",
|
||||
"hardwareGroup",
|
||||
"hardwareRow",
|
||||
"Gateway/Box 在线",
|
||||
"BOX-SIMU 端口",
|
||||
"Patch Panel 连线",
|
||||
"DO1 -> patch-panel -> DI1",
|
||||
"AI/AO/FREQ",
|
||||
"future capability",
|
||||
"SOURCE",
|
||||
"DEV-LIVE",
|
||||
"BLOCKED"
|
||||
]);
|
||||
|
||||
const workbenchMarkers = Object.freeze([
|
||||
"data-app-shell",
|
||||
"workbench-shell",
|
||||
@@ -192,6 +207,11 @@ function runStaticSmoke() {
|
||||
evidence: requiredWiringColumns
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "hardware-status-rich-structure", hardwareStatusStructureContract(files), "Right hardware status renders Gateway/Box, DI/DO/AI/AO/FREQ, patch-panel, and DO1 -> DI1 source-labeled link state.", {
|
||||
blocker: "contract_blocker",
|
||||
evidence: requiredHardwareStatusTerms
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "outer-scroll-contract", hasScrollLockContract(files.styles), "Outer page scrolling is locked and internal workbench panes own scrolling.", {
|
||||
blocker: "contract_blocker",
|
||||
evidence: ["html/body overflow hidden", "workbench-shell 100vh/100dvh overflow hidden", ".view overflow auto"]
|
||||
@@ -222,6 +242,24 @@ function runStaticSmoke() {
|
||||
evidence: ["M3 remains blocked without live loop evidence", "checked-in evidence records remain dry-run"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "m3-fixture-counts-not-dev-live", m3FixtureCountsNotDevLive(files.app), "Fixture/source patch-panel wiring plus generic runtime counts cannot render M3 DEV-LIVE.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: [
|
||||
"hasTrustedM3LiveEvidence requires patchPanelLiveReport",
|
||||
"operationId/traceId/auditId/evidenceId required",
|
||||
"runtime counts are SOURCE-only"
|
||||
]
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -432,6 +470,24 @@ function wiringTableContract(html) {
|
||||
return requiredWiringColumns.every((column) => wiringPanel.includes(`<th>${column}</th>`));
|
||||
}
|
||||
|
||||
function hardwareStatusStructureContract({ html, app, styles }) {
|
||||
const source = `${html}\n${app}\n${styles}`;
|
||||
return (
|
||||
requiredHardwareStatusTerms.every((term) => source.includes(term)) &&
|
||||
/id=["']hardware-source-status["']/u.test(html) &&
|
||||
/function\s+renderHardwareStatus\s*\(/u.test(app) &&
|
||||
/function\s+hardwareGroup\s*\(/u.test(app) &&
|
||||
/function\s+hardwareRow\s*\(/u.test(app) &&
|
||||
/function\s+trustedM3Link\s*\(/u.test(app) &&
|
||||
/function\s+hasTrustedM3LiveEvidence\s*\(/u.test(app) &&
|
||||
/function\s+linkStatusLabel\s*\(/u.test(app) &&
|
||||
/function\s+normalizePort\s*\(/u.test(app) &&
|
||||
/Web 不提供硬件写 RPC;不会新增伪硬件写操作。/u.test(app) &&
|
||||
/\.hardware-section\s*\{/u.test(styles) &&
|
||||
/\.hardware-row\s*\{/u.test(styles)
|
||||
);
|
||||
}
|
||||
|
||||
function finalRouteFallback(app) {
|
||||
const body = app.match(/function\s+routeFromLocation\s*\(\)\s*\{([\s\S]*?)\n\}/u)?.[1] ?? "";
|
||||
const returns = [...body.matchAll(/return\s+["']([^"']+)["']\s*;/gu)].map((match) => match[1]);
|
||||
@@ -517,6 +573,58 @@ function sourceEvidenceNotDevLive(source) {
|
||||
);
|
||||
}
|
||||
|
||||
function m3FixtureCountsNotDevLive(app) {
|
||||
const hardwareBody = functionBody(app, "renderHardwareStatus");
|
||||
const linkDetailBody = functionBody(app, "m3LinkDetail");
|
||||
const liveEvidenceBody = functionBody(app, "hasTrustedM3LiveEvidence");
|
||||
const patchPanelReportBody = functionBody(app, "hasTrustedPatchPanelLiveReport");
|
||||
const sourceOnlyRuntimeCounts =
|
||||
/runtimeCountsSourceKind\s*=\s*["']SOURCE["']/u.test(hardwareBody) &&
|
||||
!/sourceKind:\s*counts\s*\?\s*["']DEV-LIVE["']/u.test(hardwareBody) &&
|
||||
!/counts\s*&&\s*(?:liveLink|sourceLink|m3Link)[\s\S]{0,80}\?\s*["']DEV-LIVE["']/u.test(hardwareBody);
|
||||
return (
|
||||
sourceOnlyRuntimeCounts &&
|
||||
/hasTrustedM3LiveEvidence\(liveM3Evidence\)/u.test(hardwareBody) &&
|
||||
/patchPanelLiveReport/u.test(liveEvidenceBody) &&
|
||||
/LIVE_M3_ID_FIELDS\.every/u.test(liveEvidenceBody) &&
|
||||
/hasTrustedPatchPanelLiveReport/u.test(liveEvidenceBody) &&
|
||||
/serviceId\s*!==\s*M3_TRUSTED_ROUTE\.patchPanelServiceId/u.test(patchPanelReportBody) &&
|
||||
/trustedM3Link\(\[link\]\)/u.test(patchPanelReportBody) &&
|
||||
/counts 不是 patch-panel live report/u.test(linkDetailBody) &&
|
||||
/SOURCE fixture 包含目标接线/u.test(linkDetailBody)
|
||||
);
|
||||
}
|
||||
|
||||
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 "";
|
||||
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 "";
|
||||
}
|
||||
|
||||
function inspectHelpContract({ html, app }) {
|
||||
const markdownFiles = findHelpMarkdownFiles();
|
||||
const rendererPackages = findMarkdownRenderers();
|
||||
|
||||
+255
-61
@@ -37,6 +37,18 @@ const STATUS_LABELS = Object.freeze({
|
||||
"dev-live": "开发实况 DEV-LIVE"
|
||||
});
|
||||
|
||||
const M3_TRUSTED_ROUTE = Object.freeze({
|
||||
fromResourceId: "res_boxsimu_1",
|
||||
fromPort: "DO1",
|
||||
patchPanelServiceId: "hwlab-patch-panel",
|
||||
toResourceId: "res_boxsimu_2",
|
||||
toPort: "DI1"
|
||||
});
|
||||
|
||||
const LIVE_M3_ID_FIELDS = Object.freeze(["operationId", "traceId", "auditId", "evidenceId"]);
|
||||
const LIVE_M3_PASS_STATUSES = Object.freeze(["pass", "passed", "verified", "succeeded", "trusted", "completed"]);
|
||||
const NON_LIVE_ID_VALUES = Object.freeze(["", "n/a", "none", "null", "undefined", "not_observed", "not-observed"]);
|
||||
|
||||
const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view));
|
||||
let rpcSequence = 0;
|
||||
|
||||
@@ -65,6 +77,7 @@ const el = {
|
||||
commandInput: byId("command-input"),
|
||||
commandSend: byId("command-send"),
|
||||
commandClear: byId("command-clear"),
|
||||
hardwareSourceStatus: byId("hardware-source-status"),
|
||||
hardwareList: byId("hardware-list"),
|
||||
controlList: byId("control-list"),
|
||||
wiringBody: byId("wiring-body"),
|
||||
@@ -303,7 +316,7 @@ function renderStaticWorkbench() {
|
||||
renderUnblockList();
|
||||
renderHardwareStatus(null);
|
||||
renderDrafts();
|
||||
renderWiringList();
|
||||
renderWiringList(null);
|
||||
renderRecords(null);
|
||||
renderDiagnostics(null);
|
||||
renderGateDiagnostics();
|
||||
@@ -453,6 +466,7 @@ function renderLiveSurface(live) {
|
||||
|
||||
renderAgentChatStatus(deriveAgentChatStatus());
|
||||
renderHardwareStatus(runtimeSummary);
|
||||
renderWiringList(runtimeSummary);
|
||||
renderRecords(live);
|
||||
renderDiagnostics(live);
|
||||
renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, reachable ? "live" : "degraded");
|
||||
@@ -613,34 +627,217 @@ function renderUnblockList() {
|
||||
}
|
||||
|
||||
function renderHardwareStatus(summary) {
|
||||
const counts = summary?.counts ?? {};
|
||||
const cards = [
|
||||
const counts = summary?.counts ?? null;
|
||||
const patchPanel = gateSummary.topology.patchPanel;
|
||||
const sourceLink = trustedM3Link(patchPanel.activeConnections);
|
||||
const liveM3Evidence = trustedM3LiveEvidenceFrom(summary);
|
||||
const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence);
|
||||
const runtimeCountsSourceKind = "SOURCE";
|
||||
const runtimeCountsTone = "source";
|
||||
const patchTone = m3Live ? "live" : patchPanel.state === "active" ? "source" : patchPanel.state;
|
||||
const linkTone = m3Live ? "live" : sourceLink || patchPanel.activeConnectionCount > 0 ? "degraded" : "blocked";
|
||||
|
||||
el.hardwareSourceStatus.textContent = m3Live ? "DEV-LIVE" : "SOURCE";
|
||||
el.hardwareSourceStatus.className = `state-tag tone-${toneClass(m3Live ? "dev-live" : "source")}`;
|
||||
|
||||
replaceChildren(
|
||||
el.hardwareList,
|
||||
hardwareGroup({
|
||||
title: "Gateway/Box 在线",
|
||||
rows: [
|
||||
{
|
||||
label: "Gateway-SIMU",
|
||||
value: counts ? `${counts.gatewaySessions ?? 0} 会话` : "离线查看",
|
||||
detail: counts
|
||||
? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读 runtime counts;不是 M3 DEV-LIVE 证据。`
|
||||
: `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自 SOURCE fixture。`,
|
||||
sourceKind: runtimeCountsSourceKind,
|
||||
tone: runtimeCountsTone
|
||||
},
|
||||
{
|
||||
label: "BOX-SIMU",
|
||||
value: counts ? `${counts.boxResources ?? 0} 资源` : "离线查看",
|
||||
detail: counts
|
||||
? `${counts.boxResources ?? 0} 个 BOX 资源来自只读 runtime counts;不是 M3 DEV-LIVE 证据。`
|
||||
: `${gateSummary.boxResourceCount} 个 BOX 资源来自 SOURCE fixture。`,
|
||||
sourceKind: runtimeCountsSourceKind,
|
||||
tone: runtimeCountsTone
|
||||
}
|
||||
]
|
||||
}),
|
||||
hardwareGroup({
|
||||
title: "BOX-SIMU 端口",
|
||||
rows: [
|
||||
{
|
||||
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"
|
||||
},
|
||||
{
|
||||
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"
|
||||
},
|
||||
{
|
||||
label: "AI/AO/FREQ",
|
||||
value: "future capability",
|
||||
detail: "当前未声明模拟量/频率闭环,按 future capability 处理。",
|
||||
sourceKind: "SOURCE",
|
||||
tone: "disabled"
|
||||
}
|
||||
]
|
||||
}),
|
||||
hardwareGroup({
|
||||
title: "Patch Panel 连线",
|
||||
rows: [
|
||||
{
|
||||
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 接线;工作台只展示 required M3 route,未升级为 DEV-LIVE。`,
|
||||
sourceKind: m3Live ? "DEV-LIVE" : "SOURCE",
|
||||
tone: patchTone
|
||||
},
|
||||
{
|
||||
label: "DO1 -> patch-panel -> DI1",
|
||||
value: linkStatusLabel(linkTone),
|
||||
detail: m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }),
|
||||
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
|
||||
tone: linkTone
|
||||
}
|
||||
]
|
||||
}),
|
||||
hardwareGroup({
|
||||
title: "写入边界",
|
||||
rows: [
|
||||
{
|
||||
label: "硬件控制",
|
||||
value: "已禁用",
|
||||
detail: "Web 不提供硬件写 RPC;不会新增伪硬件写操作。",
|
||||
sourceKind: "SOURCE",
|
||||
tone: "source"
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function hardwareGroup(group) {
|
||||
const article = document.createElement("article");
|
||||
article.className = "hardware-section";
|
||||
article.append(textSpan(group.title, "hardware-section-title"));
|
||||
const rows = document.createElement("div");
|
||||
rows.className = "hardware-rows";
|
||||
rows.append(...group.rows.map(hardwareRow));
|
||||
article.append(rows);
|
||||
return article;
|
||||
}
|
||||
|
||||
function hardwareRow(item) {
|
||||
const row = document.createElement("div");
|
||||
row.className = `hardware-row tone-border-${toneClass(item.tone)}`;
|
||||
row.dataset.sourceKind = item.sourceKind;
|
||||
const head = document.createElement("div");
|
||||
head.className = "hardware-row-head";
|
||||
head.append(textSpan(item.label, "hardware-row-label"), badge(item.sourceKind, item.sourceKind));
|
||||
row.append(head);
|
||||
row.append(textSpan(item.value, `hardware-row-value tone-${toneClass(item.tone)}`));
|
||||
row.append(textSpan(item.detail, "hardware-row-detail"));
|
||||
return row;
|
||||
}
|
||||
|
||||
function trustedM3Link(connections) {
|
||||
return (connections ?? []).find(
|
||||
(link) =>
|
||||
String(link.fromResourceId ?? "") === M3_TRUSTED_ROUTE.fromResourceId &&
|
||||
normalizePort(link.fromPort) === "DO1" &&
|
||||
String(link.toResourceId ?? "") === M3_TRUSTED_ROUTE.toResourceId &&
|
||||
normalizePort(link.toPort) === "DI1" &&
|
||||
connectionPatchPanelServiceId(link) === M3_TRUSTED_ROUTE.patchPanelServiceId
|
||||
);
|
||||
}
|
||||
|
||||
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)}。`;
|
||||
}
|
||||
if (sourceLink) {
|
||||
return `BLOCKED:SOURCE fixture 包含目标接线 ${sourceLink.fromResourceId}:${sourceLink.fromPort} -> ${patchPanel.serviceId} -> ${sourceLink.toResourceId}:${sourceLink.toPort},但缺少 patch-panel live report 与 operation/trace/audit/evidence IDs。`;
|
||||
}
|
||||
if (counts) {
|
||||
return "BLOCKED:runtime counts 已返回,但 counts 不是 patch-panel live report,不能证明 M3 DEV-LIVE。";
|
||||
}
|
||||
if (patchPanel.activeConnectionCount > 0) {
|
||||
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 的可信链路证据。";
|
||||
}
|
||||
|
||||
function linkStatusLabel(status) {
|
||||
return (
|
||||
{
|
||||
title: "Gateway-SIMU",
|
||||
value: valueOrUnavailable(counts.gatewaySessions, gateSummary.gatewaySessionCount),
|
||||
meta: "只读数量;来源 SOURCE 回退",
|
||||
tone: summary ? "dev-live" : "source"
|
||||
},
|
||||
{
|
||||
title: "BOX-SIMU",
|
||||
value: valueOrUnavailable(counts.boxResources, gateSummary.boxResourceCount),
|
||||
meta: "只读数量;来源 SOURCE 回退",
|
||||
tone: summary ? "dev-live" : "source"
|
||||
},
|
||||
{
|
||||
title: "接线面板 hwlab-patch-panel",
|
||||
value: statusLabel(gateSummary.topology.patchPanel.state),
|
||||
meta: `${gateSummary.topology.patchPanel.activeConnectionCount} 条来源 SOURCE 接线`,
|
||||
tone: gateSummary.blocked ? "blocked" : gateSummary.topology.patchPanel.state
|
||||
},
|
||||
{
|
||||
title: "硬件写入界面",
|
||||
value: "已禁用",
|
||||
meta: "Web 不提供硬件写 RPC",
|
||||
tone: "source"
|
||||
}
|
||||
];
|
||||
replaceChildren(el.hardwareList, ...cards.map(statusCard));
|
||||
live: "live",
|
||||
degraded: "degraded",
|
||||
blocked: "blocked"
|
||||
}[status] ?? statusLabel(status)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePort(port) {
|
||||
return String(port ?? "").trim().toUpperCase();
|
||||
}
|
||||
|
||||
function trustedM3LiveEvidenceFrom(summary) {
|
||||
const candidates = [
|
||||
summary?.m3TrustedLoop,
|
||||
summary?.m3HardwareLoop,
|
||||
summary?.hardware?.m3TrustedLoop,
|
||||
summary?.patchPanel?.m3TrustedLoop,
|
||||
summary?.liveM3Evidence
|
||||
].filter(Boolean);
|
||||
|
||||
return candidates.find(hasTrustedM3LiveEvidence) ?? null;
|
||||
}
|
||||
|
||||
function hasTrustedM3LiveEvidence(candidate) {
|
||||
if (!candidate || typeof candidate !== "object") return false;
|
||||
if (candidate.dryRun === true || candidate.fixture === true || candidate.sourceKind === "SOURCE" || candidate.sourceKind === "DRY-RUN") return false;
|
||||
if (!hasLiveSourceKind(candidate)) return false;
|
||||
if (!LIVE_M3_PASS_STATUSES.includes(String(candidate.status ?? candidate.outcome ?? "verified").toLowerCase())) return false;
|
||||
if (!LIVE_M3_ID_FIELDS.every((field) => isLiveIdentifier(candidate[field] ?? candidate.operation?.[field] ?? candidate.identifiers?.[field]))) return false;
|
||||
const patchPanelLiveReport = candidate.patchPanelLiveReport ?? candidate.patchPanelReport;
|
||||
return hasTrustedPatchPanelLiveReport(patchPanelLiveReport);
|
||||
}
|
||||
|
||||
function hasTrustedPatchPanelLiveReport(report) {
|
||||
if (!report || typeof report !== "object") return false;
|
||||
if (report.dryRun === true || report.fixture === true || report.sourceKind === "SOURCE" || report.sourceKind === "DRY-RUN") return false;
|
||||
if (!hasLiveSourceKind(report)) return false;
|
||||
if (report.serviceId !== M3_TRUSTED_ROUTE.patchPanelServiceId) return false;
|
||||
if (!isLiveIdentifier(report.reportId ?? report.patchPanelStatusId ?? report.statusId ?? report.evidenceId)) return false;
|
||||
return (report.activeConnections ?? report.connections ?? []).some((link) => trustedM3Link([link]));
|
||||
}
|
||||
|
||||
function hasLiveSourceKind(value) {
|
||||
return value.live === true || value.sourceKind === "DEV-LIVE" || value.evidenceLevel === "DEV-LIVE";
|
||||
}
|
||||
|
||||
function isLiveIdentifier(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
return text !== "" && !NON_LIVE_ID_VALUES.includes(text.toLowerCase());
|
||||
}
|
||||
|
||||
function connectionPatchPanelServiceId(link) {
|
||||
return String(link.patchPanelServiceId ?? link.patchPanel?.serviceId ?? link.serviceId ?? M3_TRUSTED_ROUTE.patchPanelServiceId);
|
||||
}
|
||||
|
||||
function m3LiveDetail(evidence) {
|
||||
return `operation=${evidence.operationId} / trace=${evidence.traceId} / audit=${evidence.auditId} / evidence=${evidence.evidenceId}`;
|
||||
}
|
||||
|
||||
function controlRows() {
|
||||
@@ -684,35 +881,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) {
|
||||
@@ -725,13 +913,19 @@ function renderRecords(live) {
|
||||
);
|
||||
|
||||
const liveRecords = live?.evidence.ok ? live.evidence.data?.records ?? [] : [];
|
||||
const liveCards = liveRecords.map((record) =>
|
||||
infoCard({
|
||||
const liveCards = liveRecords.map((record) => {
|
||||
const m3Trusted = hasTrustedM3LiveEvidence(record);
|
||||
return infoCard({
|
||||
title: record.evidenceId,
|
||||
detail: `${recordKindLabel(record.kind ?? "record")} / ${record.operationId ?? "n/a"} / ${record.serviceId ?? "未知服务"}`,
|
||||
tone: "dev-live"
|
||||
})
|
||||
);
|
||||
detail: [
|
||||
m3Trusted ? "开发实况 DEV-LIVE M3" : "阻塞 BLOCKED:只读 evidence 查询不是 M3 trusted loop",
|
||||
recordKindLabel(record.kind ?? "record"),
|
||||
record.operationId ?? "n/a",
|
||||
record.serviceId ?? "未知服务"
|
||||
].join(" / "),
|
||||
tone: m3Trusted ? "dev-live" : "blocked"
|
||||
});
|
||||
});
|
||||
if (live && !live.evidence.ok) {
|
||||
liveCards.push(infoCard({ title: "evidence.record.query", detail: live.evidence.error, tone: "blocked" }));
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
<p class="eyebrow">运行界面</p>
|
||||
<h2>硬件状态:BOX-SIMU / Gateway-SIMU / hwlab-patch-panel</h2>
|
||||
</div>
|
||||
<span class="state-tag tone-dev-live">开发实况 DEV-LIVE 只读探测</span>
|
||||
<span class="state-tag tone-source" id="hardware-source-status">SOURCE</span>
|
||||
</div>
|
||||
<div id="hardware-list" class="hardware-list"></div>
|
||||
</section>
|
||||
|
||||
@@ -77,6 +77,42 @@ for (const legacyEnglishTitle of [
|
||||
assert.doesNotMatch(`${html}\n${app}`, legacyEnglishTitle);
|
||||
}
|
||||
assert.match(html, /硬件状态:BOX-SIMU \/ Gateway-SIMU \/ hwlab-patch-panel/);
|
||||
assert.match(html, /id="hardware-source-status"/);
|
||||
assert.match(app, /function renderHardwareStatus/);
|
||||
assert.match(app, /function hardwareGroup/);
|
||||
assert.match(app, /function hardwareRow/);
|
||||
assert.match(app, /function trustedM3Link/);
|
||||
assert.match(app, /function hasTrustedM3LiveEvidence/);
|
||||
assert.match(app, /function hasTrustedPatchPanelLiveReport/);
|
||||
assert.match(app, /function linkStatusLabel/);
|
||||
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 [
|
||||
"Gateway/Box 在线",
|
||||
"BOX-SIMU 端口",
|
||||
"Patch Panel 连线",
|
||||
"DO1 -> patch-panel -> DI1",
|
||||
"AI/AO/FREQ",
|
||||
"future capability",
|
||||
"SOURCE",
|
||||
"DEV-LIVE",
|
||||
"BLOCKED",
|
||||
"runtime counts;不是 M3 DEV-LIVE 证据",
|
||||
"counts 不是 patch-panel live report",
|
||||
"缺少 patch-panel live report 与 operation/trace/audit/evidence IDs",
|
||||
"Web 不提供硬件写 RPC;不会新增伪硬件写操作。"
|
||||
]) {
|
||||
assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`);
|
||||
}
|
||||
assert.match(app, /dataset\.sourceKind/);
|
||||
assert.match(styles, /\.hardware-section\s*{/);
|
||||
assert.match(styles, /\.hardware-row\s*{/);
|
||||
assert.match(html, /Gate \/ 诊断 \/ 验收/);
|
||||
assert.match(app, /new Set\(\["\/gate", "\/diagnostics\/gate"\]\)/);
|
||||
assert.match(html, /href="\/styles\.css"/);
|
||||
@@ -163,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"/);
|
||||
@@ -281,3 +321,18 @@ function beforeSection(source, viewId) {
|
||||
const index = source.search(new RegExp(`<section\\b[^>]*\\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 "";
|
||||
}
|
||||
|
||||
@@ -681,8 +681,74 @@ h3 {
|
||||
|
||||
.hardware-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.hardware-section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 9px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.hardware-section-title {
|
||||
color: var(--accent-2);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hardware-rows {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hardware-row {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 5px 8px;
|
||||
padding: 8px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-left-width: 3px;
|
||||
}
|
||||
|
||||
.hardware-row-head {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hardware-row-label,
|
||||
.hardware-row-value,
|
||||
.hardware-row-detail {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.hardware-row-label {
|
||||
color: var(--text);
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.hardware-row-value {
|
||||
color: var(--text);
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hardware-row-detail {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
@@ -915,7 +981,8 @@ tbody tr:last-child td {
|
||||
.tone-runtime_blocker,
|
||||
.tone-failed,
|
||||
.tone-rejected,
|
||||
.tone-unavailable {
|
||||
.tone-unavailable,
|
||||
.tone-disabled {
|
||||
color: var(--bad);
|
||||
}
|
||||
|
||||
@@ -957,10 +1024,31 @@ tbody tr:last-child td {
|
||||
.tone-bg-runtime_blocker,
|
||||
.tone-bg-failed,
|
||||
.tone-bg-rejected,
|
||||
.tone-bg-unavailable {
|
||||
.tone-bg-unavailable,
|
||||
.tone-bg-disabled {
|
||||
background: var(--bad);
|
||||
}
|
||||
|
||||
.tone-border-live,
|
||||
.tone-border-dev-live,
|
||||
.tone-border-connected,
|
||||
.tone-border-available,
|
||||
.tone-border-active,
|
||||
.tone-border-ok {
|
||||
border-left-color: var(--ok);
|
||||
}
|
||||
|
||||
.tone-border-source,
|
||||
.tone-border-disabled {
|
||||
border-left-color: var(--dim);
|
||||
}
|
||||
|
||||
.tone-border-degraded,
|
||||
.tone-border-blocked,
|
||||
.tone-border-failed {
|
||||
border-left-color: var(--bad);
|
||||
}
|
||||
|
||||
.tone-bg-dry-run,
|
||||
.tone-bg-not_run,
|
||||
.tone-bg-warn,
|
||||
|
||||
Reference in New Issue
Block a user