Merge pull request #306 from pikasTech/feat/m3-status-workspace-287

feat: add M3 status workspace
This commit is contained in:
Lyon
2026-05-23 17:07:42 +08:00
committed by GitHub
16 changed files with 1854 additions and 234 deletions
+509
View File
@@ -13,6 +13,8 @@ import {
export const M3_IO_CONTROL_CONTRACT_VERSION = "m3-io-control-v1";
export const M3_IO_CONTROL_ROUTE = "/v1/m3/io";
export const M3_STATUS_CONTRACT_VERSION = "m3-status-v1";
export const M3_STATUS_ROUTE = "/v1/m3/status";
export const M3_IO_RPC_METHODS = Object.freeze({
doWrite: "m3.io.do.write",
diRead: "m3.io.di.read"
@@ -121,6 +123,225 @@ export async function describeM3IoControlLive(options = {}) {
};
}
export async function describeM3StatusLive(options = {}) {
const env = options.env ?? process.env;
const config = buildM3IoConfig(env);
const requestJson = options.m3IoRequestJson ?? options.requestJson;
const observedAt = options.now?.() ?? new Date().toISOString();
const runtime = await safeRuntimeSummary(options.runtimeStore);
const trust = await buildM3StatusTrust({
runtimeStore: options.runtimeStore,
runtime
});
const base = {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_STATUS_CONTRACT_VERSION,
route: M3_STATUS_ROUTE,
observedAt,
chain: M3_IO_CHAIN,
boundaries: {
frontendCallsOnly: M3_STATUS_ROUTE,
cloudApiAggregatesRuntimeStatus: true,
patchPanelOwnsPropagation: true,
directFrontendGatewayOrBoxAccess: false,
sourceTopologyFallbackMustStayUnverified: true
}
};
if (!config.enabled) {
return {
...base,
status: "blocked",
sourceKind: "BLOCKED",
summary: "M3 状态聚合未启用;前端不能绕过 cloud-api 直连 gateway/box-simu。",
gateways: defaultM3Gateways({ sourceKind: "UNVERIFIED", observedAt }),
boxes: defaultM3Boxes({ sourceKind: "UNVERIFIED", observedAt }),
patchPanel: defaultM3PatchPanel({ observedAt }),
trust: {
...trust,
blocker: "m3_control_disabled"
},
blocker: {
code: "m3_control_disabled",
layer: "cloud-api",
zh: "M3 状态聚合未启用;前端不能绕过 cloud-api 直连 gateway/box-simu。"
}
};
}
const [sourceGateway, targetGateway, patchStatus, patchWiring] = await Promise.all([
readGatewaySession(config.gateway1Url, {
gatewayRole: "source",
expectedGatewayId: M3_IO_CHAIN.sourceGatewayId,
expectedGatewaySessionId: M3_IO_CHAIN.sourceGatewaySessionId,
requestJson,
timeoutMs: config.timeoutMs
}),
readGatewaySession(config.gateway2Url, {
gatewayRole: "target",
expectedGatewayId: M3_IO_CHAIN.targetGatewayId,
expectedGatewaySessionId: M3_IO_CHAIN.targetGatewaySessionId,
requestJson,
timeoutMs: config.timeoutMs
}),
requestRuntimeJson(new URL("/status", ensureTrailingSlash(config.patchPanelUrl)).toString(), {
method: "GET",
requestJson,
timeoutMs: config.timeoutMs
}),
requestRuntimeJson(new URL("/wiring", ensureTrailingSlash(config.patchPanelUrl)).toString(), {
method: "GET",
requestJson,
timeoutMs: config.timeoutMs
})
]);
const sourceCommand = normalizeCommand("do.write", { value: false });
const targetCommand = normalizeCommand("di.read", {});
const sourceBox = sourceGateway.available ? findGatewayBox(sourceGateway.status, sourceCommand) : null;
const targetBox = targetGateway.available ? findGatewayBox(targetGateway.status, targetCommand) : null;
const [sourceDoRead, targetDiRead] = await Promise.all([
sourceGateway.available && sourceBox
? readM3StatusPort({
gateway: sourceGateway,
command: sourceCommand,
requestJson,
timeoutMs: config.timeoutMs,
observedAt,
port: M3_IO_CHAIN.sourcePort,
direction: "output"
})
: Promise.resolve(unverifiedPortRead({
port: M3_IO_CHAIN.sourcePort,
direction: "output",
source: "gateway-simu",
reason: sourceGateway.reason ?? `${M3_IO_CHAIN.sourceResourceId} 未在 source gateway registry 中可见`,
observedAt
})),
targetGateway.available && targetBox
? readM3StatusPort({
gateway: targetGateway,
command: targetCommand,
requestJson,
timeoutMs: config.timeoutMs,
observedAt,
port: M3_IO_CHAIN.targetPort,
direction: "input"
})
: Promise.resolve(unverifiedPortRead({
port: M3_IO_CHAIN.targetPort,
direction: "input",
source: "gateway-simu",
reason: targetGateway.reason ?? `${M3_IO_CHAIN.targetResourceId} 未在 target gateway registry 中可见`,
observedAt
}))
]);
const gateways = [
gatewayStatusEntry(sourceGateway, {
id: M3_IO_CHAIN.sourceGatewayId,
sessionId: M3_IO_CHAIN.sourceGatewaySessionId,
role: "source",
observedAt
}),
gatewayStatusEntry(targetGateway, {
id: M3_IO_CHAIN.targetGatewayId,
sessionId: M3_IO_CHAIN.targetGatewaySessionId,
role: "target",
observedAt
})
];
const boxes = [
boxStatusEntry({
gateway: sourceGateway,
box: sourceBox,
boxId: M3_IO_CHAIN.sourceBoxId,
resourceId: M3_IO_CHAIN.sourceResourceId,
gatewayId: M3_IO_CHAIN.sourceGatewayId,
gatewaySessionId: M3_IO_CHAIN.sourceGatewaySessionId,
observedAt,
ports: {
[M3_IO_CHAIN.sourcePort]: sourceDoRead.port
}
}),
boxStatusEntry({
gateway: targetGateway,
box: targetBox,
boxId: M3_IO_CHAIN.targetBoxId,
resourceId: M3_IO_CHAIN.targetResourceId,
gatewayId: M3_IO_CHAIN.targetGatewayId,
gatewaySessionId: M3_IO_CHAIN.targetGatewaySessionId,
observedAt,
ports: {
[M3_IO_CHAIN.targetPort]: targetDiRead.port
}
})
];
const patchPanel = patchPanelStatusEntry({
statusResponse: patchStatus,
wiringResponse: patchWiring,
observedAt
});
const blockers = [
...gateways.filter((gateway) => !gateway.online).map((gateway) => ({
code: gateway.blocker,
layer: gateway.id,
zh: gateway.lastError
})),
...boxes.filter((box) => !box.online).map((box) => ({
code: M3_IO_BLOCKER_CODES.boxUnavailable,
layer: box.resourceId,
zh: `${box.resourceId} 未在 gateway-simu registry 中可见`
})),
patchPanel.connectionActive ? null : {
code: patchPanel.observable === false ? M3_IO_BLOCKER_CODES.patchPanelUnavailable : M3_IO_BLOCKER_CODES.wiringMissing,
layer: M3_IO_CHAIN.patchPanelServiceId,
zh: patchPanel.lastError ?? `hwlab-patch-panel 未确认 active ${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort} -> ${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort} 接线`
},
sourceDoRead.error ? {
code: M3_IO_BLOCKER_CODES.dispatchFailed,
layer: M3_IO_CHAIN.sourceResourceId,
zh: sourceDoRead.error
} : null,
targetDiRead.error ? {
code: M3_IO_BLOCKER_CODES.dispatchFailed,
layer: M3_IO_CHAIN.targetResourceId,
zh: targetDiRead.error
} : null
].filter(Boolean);
const hardwareLive =
blockers.length === 0 &&
sourceDoRead.port.sourceKind === "DEV-LIVE" &&
targetDiRead.port.sourceKind === "DEV-LIVE";
const trustGreen = trust.durableStatus === "green" && trust.evidenceId && trust.auditId && trust.operationId && trust.traceId;
const status = blockers.length > 0
? blockers.some((blocker) => blocker.code === M3_IO_BLOCKER_CODES.dispatchFailed || blocker.code === M3_IO_BLOCKER_CODES.patchPanelUnavailable) ? "error" : "blocked"
: trustGreen ? "live" : hardwareLive ? "blocked" : "unverified";
const sourceKind = hardwareLive ? "DEV-LIVE" : "UNVERIFIED";
return {
...base,
status,
sourceKind,
summary: status === "live"
? "M3 硬件状态与可信持久化均为 DEV-LIVE。"
: hardwareLive
? "M3 DO1/DI1 实况读数来自 cloud-api 聚合入口;可信持久化仍保持 blocked,不得标为 trusted green。"
: "M3 硬件状态尚未完整验证;SOURCE 拓扑只能作为未验证 fallback。",
gateways,
boxes,
patchPanel,
trust,
blocker: blockers[0] ?? (trust.blocker ? {
code: trust.blocker,
layer: "runtime-durable",
zh: `runtime durable 仍 blocked${trust.blocker}`
} : null)
};
}
export async function handleM3IoControl(params = {}, context = {}) {
const env = context.env ?? process.env;
const now = context.now?.() ?? new Date().toISOString();
@@ -441,6 +662,294 @@ function blockedReadiness({ checks, code, layer, reason, observedAt }) {
};
}
async function buildM3StatusTrust({ runtimeStore, runtime }) {
const durable = durableStatus(runtime);
const base = {
operationId: null,
traceId: null,
auditId: null,
evidenceId: null,
durableStatus: isDurableRuntimeReady(runtime) ? "green" : "blocked",
runtime: durable,
blocker: isDurableRuntimeReady(runtime) ? null : durable.blocker
};
const [audit, evidence] = await Promise.all([
safeRuntimeRead(() => runtimeStore?.queryAuditEvents?.({ projectId: M3_IO_CHAIN.projectId, limit: 8 })),
safeRuntimeRead(() => runtimeStore?.queryEvidenceRecords?.({ projectId: M3_IO_CHAIN.projectId, limit: 8 }))
]);
const latestAudit = latestM3AuditEvent(audit.value?.events ?? []);
const latestEvidence = latestM3EvidenceRecord(evidence.value?.records ?? []);
const operationId = latestEvidence?.operationId ?? latestAudit?.operationId ?? null;
const traceId = latestEvidence?.traceId ?? latestEvidence?.metadata?.traceId ?? latestAudit?.traceId ?? null;
return {
...base,
operationId,
traceId,
auditId: latestAudit?.auditId ?? null,
evidenceId: latestEvidence?.evidenceId ?? null,
durableStatus: base.durableStatus,
blocker: base.blocker ?? audit.error?.code ?? evidence.error?.code ?? null,
readStatus: {
audit: audit.ok ? "read" : "blocked",
evidence: evidence.ok ? "read" : "blocked",
auditError: audit.error?.message ?? null,
evidenceError: evidence.error?.message ?? null
}
};
}
async function safeRuntimeRead(read) {
if (typeof read !== "function") {
return {
ok: false,
value: null,
error: {
code: "runtime_read_unavailable",
message: "runtime read method is unavailable"
}
};
}
try {
const value = await read();
if (value === undefined || value === null) {
return {
ok: false,
value: null,
error: {
code: "runtime_read_unavailable",
message: "runtime read returned no value"
}
};
}
return {
ok: true,
value,
error: null
};
} catch (error) {
return {
ok: false,
value: null,
error: {
code: error?.code ?? "runtime_read_blocked",
message: error instanceof Error ? redactBlockerMessage(error.message) : String(error)
}
};
}
}
function latestM3AuditEvent(events) {
return [...events].reverse().find((event) =>
event?.operationId &&
event?.traceId &&
(event.action === "m3.io.do.write" || event.action === "m3.io.di.read" || String(event.metadata?.route ?? "").includes("res_boxsimu_1:DO1"))
) ?? null;
}
function latestM3EvidenceRecord(records) {
return [...records].reverse().find((record) =>
record?.operationId &&
(record.serviceId === CLOUD_API_SERVICE_ID || String(record.metadata?.route ?? "").includes("res_boxsimu_1:DO1"))
) ?? null;
}
function defaultM3Gateways({ sourceKind, observedAt }) {
return [
{
id: M3_IO_CHAIN.sourceGatewayId,
role: "source",
online: false,
observable: false,
sessionId: M3_IO_CHAIN.sourceGatewaySessionId,
lastSeenAt: observedAt,
sourceKind,
lastError: "未验证"
},
{
id: M3_IO_CHAIN.targetGatewayId,
role: "target",
online: false,
observable: false,
sessionId: M3_IO_CHAIN.targetGatewaySessionId,
lastSeenAt: observedAt,
sourceKind,
lastError: "未验证"
}
];
}
function defaultM3Boxes({ sourceKind, observedAt }) {
return [
{
id: M3_IO_CHAIN.sourceBoxId,
resourceId: M3_IO_CHAIN.sourceResourceId,
gatewayId: M3_IO_CHAIN.sourceGatewayId,
gatewaySessionId: M3_IO_CHAIN.sourceGatewaySessionId,
online: false,
observable: false,
sourceKind,
observedAt,
ports: {
[M3_IO_CHAIN.sourcePort]: {
value: null,
direction: "output",
source: "未验证",
sourceKind,
observedAt
}
}
},
{
id: M3_IO_CHAIN.targetBoxId,
resourceId: M3_IO_CHAIN.targetResourceId,
gatewayId: M3_IO_CHAIN.targetGatewayId,
gatewaySessionId: M3_IO_CHAIN.targetGatewaySessionId,
online: false,
observable: false,
sourceKind,
observedAt,
ports: {
[M3_IO_CHAIN.targetPort]: {
value: null,
direction: "input",
source: "未验证",
sourceKind,
observedAt
}
}
}
];
}
function defaultM3PatchPanel({ observedAt }) {
return {
serviceId: M3_IO_CHAIN.patchPanelServiceId,
observable: false,
connectionActive: false,
connectionVersion: null,
wiringConfigId: null,
lastSyncAt: observedAt,
lastError: "未验证",
sourceKind: "UNVERIFIED"
};
}
async function readM3StatusPort({ gateway, command, requestJson, timeoutMs, observedAt, port, direction }) {
const capability = capabilityFor({ ...command, action: "di.read", port }, {
boxId: command.boxId,
resourceId: command.resourceId
});
const dispatch = await invokeGateway(gateway.url, {
operation: "hardware.port.read",
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId: gateway.gatewaySessionId,
resourceId: command.resourceId,
boxId: command.boxId,
capabilityId: capability.capabilityId,
port,
operationId: `op_m3_status_read_${port.toLowerCase()}_${randomUUID()}`,
traceId: `trc_m3_status_read_${port.toLowerCase()}_${randomUUID()}`,
actorType: "service",
actorId: `svc_${CLOUD_API_SERVICE_ID}`,
source: "cloud-api-m3-status"
}, { requestJson, timeoutMs });
if (!dispatch.ok || dispatch.body?.accepted !== true) {
return unverifiedPortRead({
port,
direction,
source: "gateway-simu",
reason: `gateway-simu ${port} 读取失败:${dispatch.error ?? dispatch.body?.error?.message ?? "unknown"}`,
observedAt
});
}
return {
port: {
value: readValueFromGatewayResult(dispatch.body),
direction,
source: port === M3_IO_CHAIN.targetPort ? "gateway-simu/patch-panel" : "gateway-simu",
sourceKind: "DEV-LIVE",
observedAt: dispatch.body?.result?.state?.updatedAt ?? dispatch.body?.state?.updatedAt ?? observedAt,
auditId: dispatch.body?.auditId ?? dispatch.body?.result?.auditId ?? null,
evidenceId: dispatch.body?.evidenceId ?? dispatch.body?.result?.evidenceId ?? null,
state: dispatch.body?.result?.state ?? dispatch.body?.state ?? null
},
error: null
};
}
function unverifiedPortRead({ port, direction, source, reason, observedAt }) {
return {
port: {
value: null,
direction,
source,
sourceKind: "UNVERIFIED",
observedAt,
lastError: reason
},
error: reason
};
}
function gatewayStatusEntry(gateway, { id, sessionId, role, observedAt }) {
return {
id: gateway.gatewayId ?? id,
role,
online: gateway.available === true,
observable: gateway.available === true,
sessionId: gateway.gatewaySessionId ?? sessionId,
lastSeenAt: gateway.status?.lastHeartbeatAt ?? gateway.status?.updatedAt ?? gateway.status?.session?.lastSeenAt ?? observedAt,
sourceKind: gateway.available === true ? "DEV-LIVE" : "UNVERIFIED",
lastError: gateway.available === true ? null : gateway.reason,
blocker: gateway.available === true ? null : gateway.code ?? M3_IO_BLOCKER_CODES.gatewayUnavailable
};
}
function boxStatusEntry({ gateway, box, boxId, resourceId, gatewayId, gatewaySessionId, observedAt, ports }) {
return {
id: box?.boxId ?? boxId,
resourceId: box?.resourceId ?? resourceId,
gatewayId: gateway.gatewayId ?? gatewayId,
gatewaySessionId: gateway.gatewaySessionId ?? gatewaySessionId,
online: gateway.available === true && Boolean(box),
observable: gateway.available === true && Boolean(box),
sourceKind: gateway.available === true && box ? "DEV-LIVE" : "UNVERIFIED",
observedAt,
ports
};
}
function patchPanelStatusEntry({ statusResponse, wiringResponse, observedAt }) {
const status = statusResponse.body ?? {};
const wiring = wiringResponse.body ?? {};
const connectionActive = statusResponse.ok && wiringResponse.ok && hasExpectedPatchPanelWiring(status) && hasExpectedPatchPanelWiring(wiring);
const lastError = !statusResponse.ok
? `hwlab-patch-panel status 不可读:${statusResponse.error ?? `HTTP ${statusResponse.status}`}`
: !wiringResponse.ok
? `hwlab-patch-panel wiring 不可读:${wiringResponse.error ?? `HTTP ${wiringResponse.status}`}`
: connectionActive
? status.metadata?.evidenceSummary?.diagnostics?.errorCount > 0
? "hwlab-patch-panel diagnostics reports errors"
: null
: `未确认 active ${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort} -> ${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`;
return {
serviceId: status.serviceId ?? M3_IO_CHAIN.patchPanelServiceId,
observable: statusResponse.ok,
connectionActive,
connectionVersion: status.metadata?.evidenceSummary?.tickCount ?? status.metadata?.evidenceSummary?.routeCount ?? null,
wiringConfigId: status.wiringConfigId ?? wiring.wiringConfigId ?? null,
lastSyncAt: status.metadata?.evidenceSummary?.lastSyncAt ?? status.observedAt ?? wiring.updatedAt ?? observedAt,
lastError,
sourceKind: connectionActive ? "DEV-LIVE" : statusResponse.ok ? "UNVERIFIED" : "BLOCKED",
activeConnections: status.activeConnections ?? [],
diagnostics: status.metadata?.diagnostics ?? status.metadata?.evidenceSummary?.diagnostics ?? null
};
}
function readinessCheckFromGateway(id, gateway) {
return {
id,
+125 -2
View File
@@ -10,8 +10,10 @@ import {
M3_IO_BLOCKER_CODES,
M3_IO_CHAIN,
M3_IO_RPC_METHODS,
M3_STATUS_ROUTE,
describeM3IoControl,
describeM3IoControlLive,
describeM3StatusLive,
handleM3IoControl
} from "./m3-io-control.mjs";
@@ -90,6 +92,107 @@ test("M3 IO live descriptor blocks controls with Chinese reason when readiness i
assert.equal(contract.readiness.blocker.code, M3_IO_BLOCKER_CODES.wiringMissing);
});
test("M3 status live descriptor aggregates gateways, boxes, patch-panel, IO values, and blocked trust", async () => {
const fixture = createM3ControlFixture();
writeBoxPort({
state: fixture.box1,
port: "DO1",
value: true,
source: "gateway-simu"
});
writeBoxPort({
state: fixture.box2,
port: "DI1",
value: true,
source: "patch-panel",
sourceResourceId: "res_boxsimu_1",
sourcePort: "DO1",
propagatedBy: "hwlab-patch-panel"
});
const status = await describeM3StatusLive({
runtimeStore: createBlockedDurableRuntimeStore({ blocker: "runtime_durable_adapter_auth_blocked" }),
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
now: () => fixedNow,
m3IoRequestJson: fixture.requestJson
});
assert.equal(status.route, M3_STATUS_ROUTE);
assert.equal(status.status, "blocked");
assert.equal(status.sourceKind, "DEV-LIVE");
assert.equal(status.chain.sourceResourceId, "res_boxsimu_1");
assert.equal(status.boundaries.frontendCallsOnly, "/v1/m3/status");
assert.equal(status.boundaries.directFrontendGatewayOrBoxAccess, false);
assert.deepEqual(status.gateways.map((gateway) => [gateway.id, gateway.online, gateway.sessionId, gateway.sourceKind]), [
["gwsimu_1", true, "gws_gwsimu_1", "DEV-LIVE"],
["gwsimu_2", true, "gws_gwsimu_2", "DEV-LIVE"]
]);
const box1 = status.boxes.find((box) => box.resourceId === "res_boxsimu_1");
const box2 = status.boxes.find((box) => box.resourceId === "res_boxsimu_2");
assert.equal(box1.online, true);
assert.equal(box1.ports.DO1.value, true);
assert.equal(box1.ports.DO1.sourceKind, "DEV-LIVE");
assert.equal(box2.online, true);
assert.equal(box2.ports.DI1.value, true);
assert.equal(box2.ports.DI1.sourceKind, "DEV-LIVE");
assert.equal(box2.ports.DI1.source, "gateway-simu/patch-panel");
assert.equal(status.patchPanel.serviceId, "hwlab-patch-panel");
assert.equal(status.patchPanel.connectionActive, true);
assert.equal(status.patchPanel.wiringConfigId, "wir_m3_do1_di1");
assert.equal(status.patchPanel.lastError, null);
assert.equal(status.trust.durableStatus, "blocked");
assert.equal(status.trust.blocker, "runtime_durable_adapter_auth_blocked");
assert.equal(status.trust.evidenceId, null);
assert.equal(status.blocker.code, "runtime_durable_adapter_auth_blocked");
assert.deepEqual(fixture.calls.map((call) => call.path), [
"/status",
"/status",
"/status",
"/wiring",
"/invoke",
"/invoke"
]);
});
test("M3 status live descriptor keeps SOURCE fallback unverified when patch-panel route is missing", async () => {
const fixture = createM3ControlFixture({
patchPanelStatusBody: {
serviceId: "hwlab-patch-panel",
state: "active",
activeConnections: []
},
patchPanelWiringBody: {
wiringConfigId: "wir_wrong",
status: "active",
connections: []
}
});
const status = await describeM3StatusLive({
runtimeStore: createBlockedDurableRuntimeStore(),
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
now: () => fixedNow,
m3IoRequestJson: fixture.requestJson
});
assert.equal(status.status, "blocked");
assert.equal(status.patchPanel.connectionActive, false);
assert.equal(status.patchPanel.sourceKind, "UNVERIFIED");
assert.match(status.patchPanel.lastError, /未确认 active/u);
assert.equal(status.blocker.code, M3_IO_BLOCKER_CODES.wiringMissing);
assert.equal(status.boxes[0].ports.DO1.sourceKind, "DEV-LIVE");
assert.equal(status.boxes[1].ports.DI1.sourceKind, "DEV-LIVE");
assert.notEqual(status.sourceKind, "SOURCE");
});
test("M3 DO write dispatches through gateway-simu, ticks patch-panel, reads DI, and records audit/evidence fields", async () => {
const fixture = createM3ControlFixture();
const runtimeStore = createCloudRuntimeStore({
@@ -627,7 +730,7 @@ test("M3 IO fixture success cannot be promoted to DEV-LIVE without durable evide
assert.notEqual(result.auditState.sourceKind, "DEV-LIVE");
});
function createBlockedDurableRuntimeStore() {
function createBlockedDurableRuntimeStore({ blocker = "runtime_durable_adapter_query_blocked" } = {}) {
return {
async readiness() {
return {
@@ -637,7 +740,7 @@ function createBlockedDurableRuntimeStore() {
durableCapable: false,
ready: false,
status: "blocked",
blocker: "runtime_durable_adapter_query_blocked",
blocker,
reason: "Postgres runtime adapter could not complete a live runtime schema query",
liveRuntimeEvidence: false,
fixtureEvidence: false
@@ -697,6 +800,26 @@ function createM3ControlFixture({ patchPanelBody, patchPanelStatusBody, patchPan
return { ok: true, status: 200, body: gateway2 };
}
if (url.startsWith("http://gateway-1") && parsed.pathname === "/invoke") {
if (body.operation === "hardware.port.read") {
const state = box1.ports[body.port];
return {
ok: true,
status: 200,
body: {
accepted: true,
operationId: body.operationId,
traceId: body.traceId,
boxId: box1.boxId,
resourceId: box1.resource.resourceId,
capabilityId: body.capabilityId,
port: body.port,
value: state.value,
state,
auditId: "aud_box1_read",
evidenceId: "evd_box1_read"
}
};
}
return {
ok: true,
status: 200,
+7
View File
@@ -20,8 +20,10 @@ import {
import { buildCloudApiReadiness } from "./health-contract.mjs";
import {
M3_IO_CONTROL_ROUTE,
M3_STATUS_ROUTE,
describeM3IoControl,
describeM3IoControlLive,
describeM3StatusLive,
handleM3IoControl
} from "./m3-io-control.mjs";
import { createConfiguredCloudRuntimeStore } from "../db/runtime-store.mjs";
@@ -195,6 +197,11 @@ async function handleRestAdapter(request, response, url, options) {
return;
}
if (request.method === "GET" && url.pathname === M3_STATUS_ROUTE) {
sendJson(response, 200, await describeM3StatusLive(options));
return;
}
if (request.method === "POST" && url.pathname === M3_IO_CONTROL_ROUTE) {
await handleM3IoControlHttp(request, response, options);
return;
+67
View File
@@ -693,6 +693,30 @@ async function m3ReadinessRequestJson(url) {
})
};
}
if (url.startsWith("http://gateway-1") && parsed.pathname === "/invoke") {
return {
ok: true,
status: 200,
body: {
accepted: true,
operationId: "op_m3_status_do1",
traceId: "trc_m3_status_do1",
boxId: "boxsimu_1",
resourceId: "res_boxsimu_1",
port: "DO1",
value: false,
state: {
port: "DO1",
direction: "output",
value: false,
source: "gateway-simu",
updatedAt: "2026-05-23T00:00:00.000Z"
},
auditId: "aud_m3_status_do1",
evidenceId: "evd_m3_status_do1"
}
};
}
if (url.startsWith("http://gateway-2") && parsed.pathname === "/status") {
return {
ok: true,
@@ -705,6 +729,33 @@ async function m3ReadinessRequestJson(url) {
})
};
}
if (url.startsWith("http://gateway-2") && parsed.pathname === "/invoke") {
return {
ok: true,
status: 200,
body: {
accepted: true,
operationId: "op_m3_status_di1",
traceId: "trc_m3_status_di1",
boxId: "boxsimu_2",
resourceId: "res_boxsimu_2",
port: "DI1",
value: false,
state: {
port: "DI1",
direction: "input",
value: false,
source: "patch-panel",
sourceResourceId: "res_boxsimu_1",
sourcePort: "DO1",
propagatedBy: "hwlab-patch-panel",
updatedAt: "2026-05-23T00:00:00.000Z"
},
auditId: "aud_m3_status_di1",
evidenceId: "evd_m3_status_di1"
}
};
}
if (url.startsWith("http://patch-panel") && parsed.pathname === "/status") {
return {
ok: true,
@@ -1475,6 +1526,22 @@ test("cloud api /v1 exposes M3 IO control contract without generic frontend hard
assert.equal(payload.boundaries.frontendCallsOnly, "/v1/m3/io");
assert.equal(payload.boundaries.patchPanelOwnsPropagation, true);
assert.equal(payload.boundaries.genericHardwareRpcExposedToFrontend, false);
const statusResponse = await fetch(`http://127.0.0.1:${port}/v1/m3/status`);
assert.equal(statusResponse.status, 200);
const statusPayload = await statusResponse.json();
assert.equal(statusPayload.route, "/v1/m3/status");
assert.equal(statusPayload.chain.sourceResourceId, "res_boxsimu_1");
assert.equal(statusPayload.chain.targetResourceId, "res_boxsimu_2");
assert.equal(statusPayload.gateways.length, 2);
assert.equal(statusPayload.gateways.every((gateway) => gateway.online === true), true);
assert.equal(statusPayload.boxes.find((box) => box.resourceId === "res_boxsimu_1").ports.DO1.value, false);
assert.equal(statusPayload.boxes.find((box) => box.resourceId === "res_boxsimu_2").ports.DI1.value, false);
assert.equal(statusPayload.patchPanel.connectionActive, true);
assert.equal(statusPayload.trust.durableStatus, "blocked");
assert.notEqual(statusPayload.trust.durableStatus, "green");
assert.equal(statusPayload.boundaries.frontendCallsOnly, "/v1/m3/status");
assert.equal(statusPayload.boundaries.directFrontendGatewayOrBoxAccess, false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
+3 -3
View File
@@ -478,10 +478,10 @@ async function serveCloudWeb() {
return;
}
if (
(request.method === "GET" && (url.pathname === "/v1" || url.pathname.startsWith("/v1/"))) ||
if (
(request.method === "GET" && (url.pathname === "/v1" || url.pathname === "/v1/m3/status" || url.pathname.startsWith("/v1/"))) ||
(request.method === "POST" && (url.pathname === "/json-rpc" || url.pathname === "/v1/agent/chat" || url.pathname === "/v1/m3/io"))
) {
) {
await proxyCloudApi(request, response, url);
return;
}
+3
View File
@@ -7,6 +7,7 @@ import {
parseSmokeArgs,
printSmokeHelp,
runDevCloudWorkbenchMobileSmoke,
runM3StatusFixtureSmoke,
runDevCloudWorkbenchSmoke
} from "./src/dev-cloud-workbench-smoke-lib.mjs";
@@ -52,6 +53,8 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
? printSmokeHelp()
: args.mobile
? await runDevCloudWorkbenchMobileSmoke()
: args.mode === "m3-status-fixture"
? await runM3StatusFixtureSmoke()
: await runDevCloudWorkbenchSmoke(process.argv.slice(2));
const reportWrite = decideSmokeReportWrite(args, report);
if (reportWrite.write) {
+1 -1
View File
@@ -505,7 +505,7 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
assert.equal(Array.isArray(report.failures), true);
assert.equal(Array.isArray(report.skipped), true);
assert.equal(report.safety.hitTestMethod.includes("elementsFromPoint"), true);
assert.equal(report.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.status, "skip");
assert.equal(report.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.status, "pass");
assert.equal(report.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status, "skip");
const desktopCollapsed = report.checks.find((check) => check.id === "layout-desktop-collapsed")?.observations;
+3 -3
View File
@@ -225,7 +225,7 @@ test("M3 IO E2E frontend guardrail requires readiness before controls unlock", (
`;
const pass = checkFrontendNoDirectRuntimeCalls({
appSource,
artifactPublisherSource: 'if (url.pathname === "/v1/m3/io") proxyCloudApi();'
artifactPublisherSource: 'if (url.pathname === "/v1/m3/io" || url.pathname === "/v1/m3/status") proxyCloudApi();'
});
assert.equal(pass.status, "pass");
@@ -238,7 +238,7 @@ test("M3 IO E2E frontend guardrail requires readiness before controls unlock", (
return fetchJson("/v1/m3/io", { method: "POST" });
}
`,
artifactPublisherSource: 'if (url.pathname === "/v1/m3/io") proxyCloudApi();'
artifactPublisherSource: 'if (url.pathname === "/v1/m3/io" || url.pathname === "/v1/m3/status") proxyCloudApi();'
});
assert.equal(fail.status, "fail");
assert.ok(fail.issues.some((issue) => /readiness/u.test(issue)));
@@ -263,7 +263,7 @@ test("M3 IO E2E frontend guardrail catches direct gateway or patch-panel calls",
`;
const pass = checkFrontendNoDirectRuntimeCalls({
appSource,
artifactPublisherSource: 'if (url.pathname === "/v1/m3/io") proxyCloudApi();'
artifactPublisherSource: 'if (url.pathname === "/v1/m3/io" || url.pathname === "/v1/m3/status") proxyCloudApi();'
});
assert.equal(pass.status, "pass");
+610 -42
View File
@@ -133,17 +133,21 @@ const requiredTrustedRecordTerms = Object.freeze([
const requiredHardwareStatusTerms = Object.freeze([
"hardware-source-status",
"hardwareGroup",
"hardwareRow",
"Gateway/Box 在线",
"BOX-SIMU 端口",
"Patch Panel 连线",
"DO1 -> patch-panel -> DI1",
"AI/AO/FREQ",
"后续能力",
"hardware-status-tabs",
"hardwareKvGroup",
"/v1/m3/status",
"Gateway-SIMU",
"BOX-SIMU-1",
"BOX-SIMU-2",
"Patch Panel",
"Gateway 在线",
"BOX 在线",
"patch-panel",
"runtime durable",
"DISABLED 后续能力",
"SOURCE",
"DEV-LIVE",
"BLOCKED"
"unverified_cloud_api_m3_status"
]);
const workbenchMarkers = Object.freeze([
@@ -231,6 +235,7 @@ export async function runDevCloudWorkbenchSmoke(argv = []) {
if (args.mode === "live") return runLiveSmoke(args);
if (args.mode === "local-agent-timeout-fixture") return runDevCloudWorkbenchTimeoutFixtureSmoke();
if (args.mode === "dom-only") return runLiveDomOnlySmoke(args);
if (args.mode === "m3-status-fixture") return runM3StatusFixtureSmoke();
if (args.mode === "layout") return runDevCloudWorkbenchLayoutSmoke(args);
if (args.mode === "local-agent-fixture") return runDevCloudWorkbenchLocalAgentFixtureSmoke();
if (args.mode === "mobile") return runDevCloudWorkbenchMobileSmoke();
@@ -283,6 +288,8 @@ export function parseSmokeArgs(argv) {
args.confirmDevLive = true;
} else if (arg === "--dom-only") {
args.mode = "dom-only";
} else if (arg === "--m3-status-fixture") {
args.mode = "m3-status-fixture";
} else if (arg === "--layout") {
args.mode = "layout";
} else if (arg === "--build") {
@@ -416,8 +423,8 @@ function runStaticSmoke() {
"收起左侧资源树 / 展开左侧资源树 aria-label",
"收起资源树 / 展开资源树 visible button text",
"explorer-collapsed sets explorer column to 0",
"collapsed desktop center column minmax(560px, 1fr)",
"right side keeps a future width boundary through --right-width"
"collapsed desktop center column minmax(420px, 1fr)",
"right side keeps a 560-740px M3 status workspace boundary through --right-width"
]
});
@@ -426,9 +433,9 @@ function runStaticSmoke() {
evidence: ["/health/live", "/v1", "/v1/agent/chat", "/json-rpc", ...readOnlyRpcMethods]
});
addCheck(checks, blockers, "api-degraded-default-status", hasApiDegradedDefaultStatus(files), "Default workbench status distinguishes reachable degraded API from fully OK without exposing backend blocker details.", {
addCheck(checks, blockers, "api-degraded-default-status", hasApiDegradedDefaultStatus(files), "Default workbench status distinguishes reachable blocked API from fully OK with concrete blocker copy.", {
blocker: "observability_blocker",
evidence: ["healthLive.data.status === degraded", "ready false", "runtime_durable_adapter_query_blocked", "API 降级 / 只读模式", "可信记录受阻 / 只读模式", "no Gate/diagnostics/blocker copy in default status classifier"]
evidence: ["healthLive.data.status === degraded", "ready false", "runtime_durable_adapter_query_blocked", "runtime_durable_adapter_auth_blocked", "API 错误 / 只读模式", "可信记录受阻 / 只读模式", "no Gate/diagnostics copy in default status classifier"]
});
addCheck(checks, blockers, "code-agent-chat-primary-flow", hasCodeAgentChatContract(files), "Center Agent conversation sends to the controlled Code Agent chat endpoint and no longer uses 添加草稿 as the primary flow.", {
@@ -494,7 +501,7 @@ 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",
"renderHardwareStatus renders tabbed Key/Value groups",
"renderWiringList uses M3_TRUSTED_ROUTE",
"M5 activeConnections are not mapped into the workbench wiring table"
]
@@ -1734,16 +1741,27 @@ function hardwareStatusStructureContract({ html, app, styles }) {
return (
requiredHardwareStatusTerms.every((term) => source.includes(term)) &&
/id=["']hardware-source-status["']/u.test(html) &&
/data-hardware-tab=["']overview["'][\s\S]*?>总览<\/button>/u.test(html) &&
/data-hardware-tab=["']gateways["'][\s\S]*?>Gateway-SIMU<\/button>/u.test(html) &&
/data-hardware-tab=["']box1["'][\s\S]*?>BOX-SIMU-1<\/button>/u.test(html) &&
/data-hardware-tab=["']box2["'][\s\S]*?>BOX-SIMU-2<\/button>/u.test(html) &&
/data-hardware-tab=["']patch["'][\s\S]*?>Patch Panel<\/button>/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+initHardwareTabs\s*\(/u.test(app) &&
/function\s+hardwareKvGroup\s*\(/u.test(app) &&
/function\s+sourceFallbackM3Status\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) &&
/页面不直连模拟器,也不伪造硬件状态。/u.test(app) &&
/fetchJson\("\/v1\/m3\/status"\)/u.test(app) &&
/sourceFallbackM3Status\(\)/u.test(functionBody(app, "renderHardwareStatus")) &&
/\.hardware-section\s*\{/u.test(styles) &&
/\.hardware-row\s*\{/u.test(styles)
/\.hardware-status-tabs\s*\{/u.test(styles) &&
/\.hardware-tab\s*\{/u.test(styles) &&
/\.kv-list\s*\{/u.test(styles) &&
/\.kv-row\s*\{/u.test(styles)
);
}
@@ -1850,9 +1868,9 @@ function hasSidebarCollapseContract({ html, app, styles }) {
/textContent = collapsed \? "展开资源树" : "收起资源树"/u.test(setCollapsedBody) &&
/title = collapsed \? "展开左侧资源树,恢复导航和硬件资源" : "收起左侧资源树,给工作区更多宽度"/u.test(setCollapsedBody) &&
/--explorer-width:\s*292px/u.test(styles) &&
/--right-width:\s*clamp\(366px,\s*28vw,\s*440px\)/u.test(styles) &&
/--right-width-expanded:\s*clamp\(366px,\s*26vw,\s*410px\)/u.test(styles) &&
/\.explorer-collapsed\s*\{[^}]*--explorer-width:\s*0px;[^}]*grid-template-columns:\s*var\(--rail-width\)\s+0\s+minmax\(560px,\s*1fr\)\s+var\(--right-width\);/su.test(styles) &&
/--right-width:\s*clamp\(560px,\s*38vw,\s*740px\)/u.test(styles) &&
/--right-width-expanded:\s*clamp\(560px,\s*45vw,\s*740px\)/u.test(styles) &&
/\.explorer-collapsed\s*\{[^}]*--explorer-width:\s*0px;[^}]*grid-template-columns:\s*var\(--rail-width\)\s+0\s+minmax\(420px,\s*1fr\)\s+var\(--right-width\);/su.test(styles) &&
/\.side-panel\s*\{[^}]*overflow:\s*auto;[^}]*overscroll-behavior:\s*contain;/su.test(styles)
);
}
@@ -1876,6 +1894,7 @@ function hasSameOriginReadOnlyBoundary(app) {
return (
/fetchJson\("\/health\/live"\)/u.test(app) &&
/fetchJson\("\/v1"\)/u.test(app) &&
/fetchJson\("\/v1\/m3\/status"\)/u.test(app) &&
/fetchJson\("\/v1\/agent\/chat"/u.test(app) &&
/fetchJson\("\/json-rpc"/u.test(app) &&
JSON.stringify(rpcCalls) === JSON.stringify([...readOnlyRpcMethods].sort()) &&
@@ -1898,13 +1917,16 @@ function hasApiDegradedDefaultStatus({ app }) {
/payload\.readiness\?\.ready/u.test(functionBody(app, "readinessBooleanValues")) &&
/readyValues\.includes\(false\)/u.test(readinessBody) &&
/runtime_durable_adapter_query_blocked/u.test(durableBlockBody) &&
/runtime_durable_adapter_/u.test(durableBlockBody) &&
/payload\.runtime\?\.blocker/u.test(durableBlockBody) &&
/payload\.readiness\?\.durability\?\.blocker/u.test(durableBlockBody) &&
/blockedLayer === "durability_query"/u.test(durableBlockBody) &&
/可信记录受阻 \/ 只读模式/u.test(statusBody) &&
/API 降级 \/ 只读模式/u.test(statusBody) &&
/同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。/u.test(statusBody) &&
/同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。/u.test(statusBody) &&
/API 错误 \/ 只读模式/u.test(statusBody) &&
/apiDurableBlockerDetail\(coreProbes\)/u.test(statusBody) &&
/apiProbeErrorDetail\(coreProbes\)/u.test(statusBody) &&
/\/health\/live 可响应,但可信记录持久化尚未就绪:/u.test(app) &&
!/API 降级 \/ 只读模式/u.test(app) &&
!/runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u.test(statusBody)
);
}
@@ -2059,32 +2081,33 @@ function sourceEvidenceNotDevLive(source) {
function m3FixtureCountsNotDevLive(app) {
const hardwareBody = functionBody(app, "renderHardwareStatus");
const linkDetailBody = functionBody(app, "m3LinkDetail");
const fallbackBody = functionBody(app, "sourceFallbackM3Status");
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);
const sourceFallbackStaysUnverified =
/sourceKind:\s*"SOURCE"/u.test(fallbackBody) &&
/unverified_cloud_api_m3_status/u.test(fallbackBody) &&
/SOURCE 拓扑不能冒充实况硬件状态/u.test(fallbackBody) &&
!/sourceKind:\s*"DEV-LIVE"/u.test(fallbackBody);
return (
sourceOnlyRuntimeCounts &&
/hasTrustedM3LiveEvidence\(liveM3Evidence\)/u.test(hardwareBody) &&
sourceFallbackStaysUnverified &&
/sourceFallbackM3Status\(\)/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) &&
/只读运行计数已返回,但还不能证明接线盘闭环/u.test(linkDetailBody) &&
/来源拓扑包含目标接线/u.test(linkDetailBody)
/trustedM3Link\(\[link\]\)/u.test(patchPanelReportBody)
);
}
function m3RenderedWorkbenchNotM5Fixture(app) {
const hardwareBody = functionBody(app, "renderHardwareStatus");
const hardwareGroupBody = functionBody(app, "hardwareGroup");
const overviewBody = functionBody(app, "hardwareOverviewGroups");
const wiringBody = functionBody(app, "renderWiringList");
return (
/group\.rows\.map\(hardwareRow\)/u.test(hardwareGroupBody) &&
/Key\/Value groups/u.test(hardwareBody) &&
/M3_TRUSTED_ROUTE\.fromResourceId/u.test(overviewBody) &&
/M3_TRUSTED_ROUTE\.toResourceId/u.test(overviewBody) &&
!/hardwareRow\(\s*\{/u.test(hardwareBody) &&
/M3_TRUSTED_ROUTE\.fromResourceId/u.test(wiringBody) &&
/M3_TRUSTED_ROUTE\.patchPanelServiceId/u.test(wiringBody) &&
@@ -3124,8 +3147,11 @@ async function inspectDefaultApiStatus(page) {
methodText,
firstScreenText,
degradedStatusVisible:
liveStatus === "可信记录受阻 / 只读模式" &&
liveDetail === "同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。" &&
(liveStatus === "可信记录受阻 / 只读模式" || liveStatus === "API 错误 / 只读模式") &&
(
liveDetail.startsWith("/health/live 可响应,但可信记录持久化尚未就绪:") ||
liveDetail === "同源 API 返回 blocked/degraded/failed 或 ready=false;当前保持只读查看和本地任务整理。"
) &&
methodText.includes("system.health"),
defaultCopyIsWorkbenchSafe: !forbidden.test(`${liveStatus} ${liveDetail} ${firstScreenText}`)
};
@@ -3302,7 +3328,7 @@ async function runLocalAgentFixtureSmoke({
{
id: "local-api-degraded-default-status",
status: apiStatus.degradedStatusVisible && apiStatus.defaultCopyIsWorkbenchSafe ? "pass" : "blocked",
summary: "Default workbench status renders reachable degraded /health/live as API degraded read-only without first-screen backend blocker details.",
summary: "Default workbench status renders reachable blocked /health/live as concrete read-only status without first-screen backend route details.",
observations: apiStatus
},
{
@@ -3588,10 +3614,142 @@ export async function runDevCloudWorkbenchMobileSmoke() {
}
}
export async function runM3StatusFixtureSmoke() {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
const summary = `M3 status fixture smoke skipped because Playwright is unavailable: ${error.message}`;
return {
status: "skip",
task: "DC-DCSN-P0-2026-003",
mode: "m3-status-fixture",
evidenceLevel: "SOURCE",
devLive: false,
summary,
checks: [
{
id: "m3-status-browser-dependency",
status: "skip",
summary: playwrightDependencySummary,
evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"]
}
],
blockers: [
{
type: "environment_blocker",
scope: "m3-status-browser-dependency",
status: "open",
summary
}
],
safety: staticSafety()
};
}
const m3State = {
doValue: false,
diValue: false,
observedAt: "2026-05-23T00:00:00.000Z",
sequence: 1
};
const server = await startStaticWebServer({ m3StatusFixture: true, m3State });
let browser;
try {
browser = await chromium.launch({ headless: true });
const desktop = await inspectM3StatusFixtureViewport(browser, server.url, {
width: 1366,
height: 768,
expectedRightWidthMin: 560
});
const mobile = await inspectM3StatusFixtureViewport(browser, server.url, {
width: 390,
height: 844,
expectedRightWidthMin: 300
});
const refresh = await inspectM3StatusFixtureRefresh(browser, server.url);
const checks = [
{
id: "m3-status-fixture-desktop",
status: desktop.pass ? "pass" : "blocked",
summary: "Desktop hardware workspace reads /v1/m3/status, exposes tabs/Key-Value state, and has no outer or horizontal scroll.",
observations: desktop
},
{
id: "m3-status-fixture-mobile",
status: mobile.pass ? "pass" : "blocked",
summary: "390x844 hardware workspace wraps tabs, preserves Key-Value content, and avoids horizontal overflow.",
observations: mobile
},
{
id: "m3-status-fixture-refresh",
status: refresh.pass ? "pass" : "blocked",
summary: "DO1 false/true writes refresh through same-origin cloud-api status and DI1 follows through patch-panel.",
observations: refresh
}
];
const blockers = checks
.filter((check) => check.status !== "pass")
.map((check) => ({
type: "runtime_blocker",
scope: check.id,
status: "open",
summary: check.summary
}));
return {
status: blockers.length === 0 ? "pass" : "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "m3-status-fixture",
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: "Local SOURCE fixture browser smoke validates the M3 status UI contract; it is not DEV-LIVE hardware evidence.",
refs: ["pikasTech/HWLAB#287", "pikasTech/HWLAB#273", "pikasTech/HWLAB#278"],
checks,
blockers,
safety: {
...staticSafety(),
devLive: false,
sourceIsDevLive: false,
hardwareWriteApis: false,
statement: "Fixture smoke intercepts only same-origin cloud-api routes on a local static server; it never contacts gateway-simu, box-simu, patch-panel, DEV, or PROD."
}
};
} catch (error) {
return {
status: "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "m3-status-fixture",
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: `M3 status fixture smoke failed: ${error.message}`,
checks: [],
blockers: [
{
type: "runtime_blocker",
scope: "m3-status-fixture",
status: "open",
summary: error.message
}
],
safety: staticSafety()
};
} finally {
if (browser) await browser.close();
await server.close();
}
}
async function startStaticWebServer(options = {}) {
const rootDir = path.resolve(options.rootDir ?? webRoot);
const server = http.createServer(async (request, response) => {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
if (options.m3StatusFixture && await handleM3StatusFixtureApi({ request, response, url, options })) {
return;
}
if (options.agentFixture && await handleLocalAgentFixtureApi({ request, response, url, options })) {
return;
}
@@ -3625,6 +3783,219 @@ async function startStaticWebServer(options = {}) {
};
}
async function handleM3StatusFixtureApi({ request, response, url, options = {} }) {
if (request.method === "GET" && (url.pathname === "/health/live" || url.pathname === "/v1")) {
jsonResponse(response, 200, m3StatusFixtureHealth(options.m3State));
return true;
}
if (request.method === "GET" && url.pathname === "/v1/m3/io") {
jsonResponse(response, 200, {
status: "available",
route: "/v1/m3/io",
readiness: {
status: "ready",
controlReady: true,
sourceKind: "DEV-LIVE",
evidenceLevel: "DEV-LIVE"
}
});
return true;
}
if (request.method === "GET" && url.pathname === "/v1/m3/status") {
jsonResponse(response, 200, m3StatusFixturePayload(options.m3State));
return true;
}
if (request.method === "POST" && url.pathname === "/v1/m3/io") {
const body = await readJsonBody(request);
if (body?.action === "do.write") {
options.m3State.doValue = body.value === true;
options.m3State.diValue = body.value === true;
options.m3State.sequence += 1;
options.m3State.observedAt = `2026-05-23T00:00:0${Math.min(options.m3State.sequence, 9)}.000Z`;
}
jsonResponse(response, 200, {
status: "succeeded",
accepted: true,
action: body?.action ?? "di.read",
value: body?.action === "di.read" ? options.m3State.diValue : options.m3State.doValue,
result: {
value: body?.action === "di.read" ? options.m3State.diValue : options.m3State.doValue
},
operationId: `op_fixture_${options.m3State.sequence}`,
traceId: `trc_fixture_${options.m3State.sequence}`,
auditId: null,
evidenceId: null,
evidenceState: {
status: "blocked",
reason: "runtime_durable_adapter_auth_blocked"
},
controlPath: {
frontendBypass: false
}
});
return true;
}
if (request.method === "POST" && url.pathname === "/json-rpc") {
const body = await readJsonBody(request);
jsonResponse(response, 200, {
jsonrpc: "2.0",
id: body?.id ?? "req_m3_status_fixture",
result: localRpcFixtureResult(body?.method)
});
return true;
}
return false;
}
function m3StatusFixtureHealth(m3State = { sequence: 1 }) {
return {
serviceId: "hwlab-cloud-api",
status: "degraded",
ready: false,
methods: [...readOnlyRpcMethods],
runtime: {
durable: true,
ready: false,
liveRuntimeEvidence: false,
status: "blocked",
blocker: "runtime_durable_adapter_auth_blocked",
connection: {
queryAttempted: true,
queryResult: "auth_blocked"
}
},
readiness: {
status: "degraded",
ready: false,
durability: {
status: "blocked",
ready: false,
blocker: "runtime_durable_adapter_auth_blocked",
blockedLayer: "durability_auth",
queryResult: "auth_blocked"
}
},
blockerCodes: ["runtime_durable_adapter_auth_blocked"],
m3FixtureSequence: m3State.sequence
};
}
function m3StatusFixturePayload(m3State = { doValue: false, diValue: false, observedAt: "2026-05-23T00:00:00.000Z", sequence: 1 }) {
const observedAt = m3State.observedAt;
return {
serviceId: "hwlab-cloud-api",
contractVersion: "m3-status-v1",
route: "/v1/m3/status",
status: "blocked",
sourceKind: "DEV-LIVE",
observedAt,
chain: {
projectId: gateSummary.topology.projectId,
sourceGatewayId: "gwsimu_1",
sourceGatewaySessionId: "gws_gwsimu_1",
sourceResourceId: "res_boxsimu_1",
sourceBoxId: "boxsimu_1",
sourcePort: "DO1",
targetGatewayId: "gwsimu_2",
targetGatewaySessionId: "gws_gwsimu_2",
targetResourceId: "res_boxsimu_2",
targetBoxId: "boxsimu_2",
targetPort: "DI1",
patchPanelServiceId: "hwlab-patch-panel"
},
boundaries: {
frontendCallsOnly: "/v1/m3/status",
directFrontendGatewayOrBoxAccess: false,
sourceTopologyFallbackMustStayUnverified: true
},
gateways: [
{
id: "gwsimu_1",
role: "source",
online: true,
observable: true,
sessionId: "gws_gwsimu_1",
lastSeenAt: observedAt,
sourceKind: "DEV-LIVE",
lastError: null
},
{
id: "gwsimu_2",
role: "target",
online: true,
observable: true,
sessionId: "gws_gwsimu_2",
lastSeenAt: observedAt,
sourceKind: "DEV-LIVE",
lastError: null
}
],
boxes: [
{
id: "boxsimu_1",
resourceId: "res_boxsimu_1",
gatewayId: "gwsimu_1",
gatewaySessionId: "gws_gwsimu_1",
online: true,
observable: true,
sourceKind: "DEV-LIVE",
observedAt,
ports: {
DO1: {
value: m3State.doValue,
direction: "output",
source: "gateway-simu",
sourceKind: "DEV-LIVE",
observedAt
}
}
},
{
id: "boxsimu_2",
resourceId: "res_boxsimu_2",
gatewayId: "gwsimu_2",
gatewaySessionId: "gws_gwsimu_2",
online: true,
observable: true,
sourceKind: "DEV-LIVE",
observedAt,
ports: {
DI1: {
value: m3State.diValue,
direction: "input",
source: "gateway-simu/patch-panel",
sourceKind: "DEV-LIVE",
observedAt
}
}
}
],
patchPanel: {
serviceId: "hwlab-patch-panel",
observable: true,
connectionActive: true,
connectionVersion: m3State.sequence,
wiringConfigId: "wir_m3_do1_di1",
lastSyncAt: observedAt,
lastError: null,
sourceKind: "DEV-LIVE"
},
trust: {
operationId: `op_fixture_${m3State.sequence}`,
traceId: `trc_fixture_${m3State.sequence}`,
auditId: null,
evidenceId: null,
durableStatus: "blocked",
blocker: "runtime_durable_adapter_auth_blocked"
},
blocker: {
code: "runtime_durable_adapter_auth_blocked",
layer: "runtime-durable",
zh: "runtime durable 仍 blockedruntime_durable_adapter_auth_blocked"
}
};
}
async function handleLocalAgentFixtureApi({ request, response, url, options = {} }) {
if (request.method === "GET" && (url.pathname === "/health/live" || url.pathname === "/v1")) {
jsonResponse(response, 200, {
@@ -3715,6 +4086,160 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
return false;
}
async function inspectM3StatusFixtureViewport(browser, url, viewport) {
const page = await browser.newPage({ viewport: { width: viewport.width, height: viewport.height } });
const requests = [];
try {
page.on("request", (request) => {
const requestUrl = new URL(request.url());
requests.push({
method: request.method(),
pathname: requestUrl.pathname,
href: request.url()
});
});
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10000 });
await page.waitForFunction(() => document.querySelector("#hardware-source-status")?.textContent?.trim() === "实况受阻", null, { timeout: 8000 });
for (const tab of ["gateways", "box1", "box2", "patch", "overview"]) {
await page.locator(`[data-hardware-tab="${tab}"]`).click();
}
const dom = await page.evaluate(() => {
const text = document.body.textContent ?? "";
const hardwareText = document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const right = document.querySelector(".right-sidebar");
const hardwareList = document.querySelector("#hardware-list");
const tabs = [...document.querySelectorAll("[data-hardware-tab]")];
const tabRows = new Set(tabs.map((tab) => Math.round(tab.getBoundingClientRect().top))).size;
const tabLabels = tabs.map((tab) => tab.textContent?.trim() ?? "");
const visible = (selector) => {
const element = document.querySelector(selector);
if (!element) return false;
const box = element.getBoundingClientRect();
const style = getComputedStyle(element);
return box.width > 0 && box.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
return {
hardwareStatus: document.querySelector("#hardware-source-status")?.textContent?.trim() ?? "",
liveDetail: document.querySelector("#live-detail")?.textContent?.trim() ?? "",
hardwareText,
tabLabels,
tabRows,
rightWidth: right?.getBoundingClientRect().width ?? 0,
rightScrollWidth: right?.scrollWidth ?? 0,
rightClientWidth: right?.clientWidth ?? 0,
hardwareScrollWidth: hardwareList?.scrollWidth ?? 0,
hardwareClientWidth: hardwareList?.clientWidth ?? 0,
htmlScrollWidth: document.documentElement.scrollWidth,
htmlClientWidth: document.documentElement.clientWidth,
bodyScrollWidth: document.body.scrollWidth,
bodyClientWidth: document.body.clientWidth,
outerScrollLocked:
getComputedStyle(document.documentElement).overflow === "hidden" &&
getComputedStyle(document.body).overflow === "hidden" &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2,
keysVisible: ["状态", "Gateway 在线", "BOX 在线", "DO1", "DI1", "runtime durable", "blocker"].every((term) => hardwareText.includes(term)),
tabsVisible: ["总览", "Gateway-SIMU", "BOX-SIMU-1", "BOX-SIMU-2", "Patch Panel"].every((term) => tabLabels.includes(term)),
blockedVisible: text.includes("runtime_durable_adapter_auth_blocked") && !text.includes("实况可信"),
noHorizontalOverflow:
document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 &&
document.body.scrollWidth <= document.body.clientWidth + 2 &&
(!right || right.scrollWidth <= right.clientWidth + 2) &&
(!hardwareList || hardwareList.scrollWidth <= hardwareList.clientWidth + 2),
boxTabClickable: visible('[data-hardware-tab="box1"]') && visible('[data-hardware-tab="box2"]')
};
});
const directRuntimeRequests = requests.filter((request) => /gateway-simu|box-simu|patch-panel|:7101|:7201|:7301/iu.test(request.href));
const pass =
dom.hardwareStatus === "实况受阻" &&
dom.tabsVisible &&
dom.keysVisible &&
dom.blockedVisible &&
dom.outerScrollLocked &&
dom.noHorizontalOverflow &&
dom.boxTabClickable &&
dom.rightWidth >= viewport.expectedRightWidthMin &&
requests.some((request) => request.method === "GET" && request.pathname === "/v1/m3/status") &&
directRuntimeRequests.length === 0;
return {
viewport: { width: viewport.width, height: viewport.height },
pass,
...dom,
statusRequests: requests.filter((request) => request.pathname === "/v1/m3/status").length,
directRuntimeRequests
};
} finally {
await page.close();
}
}
async function inspectM3StatusFixtureRefresh(browser, url) {
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
const requests = [];
try {
page.on("request", (request) => {
const requestUrl = new URL(request.url());
requests.push({
method: request.method(),
pathname: requestUrl.pathname,
href: request.url()
});
});
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10000 });
await page.waitForFunction(() => document.querySelector("#hardware-source-status")?.textContent?.trim() === "实况受阻", null, { timeout: 8000 });
await page.locator('[data-hardware-tab="box1"]').click();
const initialBox1 = await hardwarePanelText(page);
await page.locator("#m3-value-select").selectOption("true");
await page.locator("#m3-write-do").click();
await page.waitForFunction(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, "").includes("DO1true"), null, { timeout: 8000 });
const trueBox1 = await hardwarePanelText(page);
await page.locator('[data-hardware-tab="box2"]').click();
await page.waitForFunction(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, "").includes("DI1true"), null, { timeout: 8000 });
const trueBox2 = await hardwarePanelText(page);
await page.locator("#m3-value-select").selectOption("false");
await page.locator("#m3-write-do").click();
await page.waitForFunction(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, "").includes("DI1false"), null, { timeout: 8000 });
await page.locator('[data-hardware-tab="box1"]').click();
await page.waitForFunction(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, "").includes("DO1false"), null, { timeout: 8000 });
const falseBox1 = await hardwarePanelText(page);
await page.locator('[data-hardware-tab="box2"]').click();
await page.waitForFunction(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, "").includes("DI1false"), null, { timeout: 8000 });
const falseBox2 = await hardwarePanelText(page);
const statusRequests = requests.filter((request) => request.method === "GET" && request.pathname === "/v1/m3/status").length;
const ioPosts = requests.filter((request) => request.method === "POST" && request.pathname === "/v1/m3/io").length;
const directRuntimeRequests = requests.filter((request) => /gateway-simu|box-simu|patch-panel|:7101|:7201|:7301/iu.test(request.href));
const blockedStillVisible = await page.evaluate(() => document.body.textContent?.includes("runtime_durable_adapter_auth_blocked") === true && !document.body.textContent?.includes("实况可信"));
const pass =
initialBox1.includes("DO1false") &&
trueBox1.includes("DO1true") &&
trueBox2.includes("DI1true") &&
falseBox1.includes("DO1false") &&
falseBox2.includes("DI1false") &&
statusRequests >= 3 &&
ioPosts === 2 &&
directRuntimeRequests.length === 0 &&
blockedStillVisible;
return {
pass,
initialBox1,
trueBox1,
trueBox2,
falseBox1,
falseBox2,
statusRequests,
ioPosts,
directRuntimeRequests,
blockedStillVisible
};
} finally {
await page.close();
}
}
async function hardwarePanelText(page) {
return page.evaluate(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, " ").trim() ?? "");
}
function delay(ms) {
const normalized = Number.isFinite(Number(ms)) && Number(ms) > 0 ? Number(ms) : 0;
return new Promise((resolve) => setTimeout(resolve, normalized));
@@ -4029,7 +4554,12 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
["#tab-wiring", "M3 接线标签"],
["#tab-records", "可信记录标签"],
["#m3-write-do", "写入 DO1"],
["#m3-read-di", "读取 DI1"]
["#m3-read-di", "读取 DI1"],
["[data-hardware-tab='overview']", "硬件总览标签"],
["[data-hardware-tab='gateways']", "Gateway-SIMU 标签"],
["[data-hardware-tab='box1']", "BOX-SIMU-1 标签"],
["[data-hardware-tab='box2']", "BOX-SIMU-2 标签"],
["[data-hardware-tab='patch']", "Patch Panel 标签"]
];
const overflowSelectors = [
"body",
@@ -4074,10 +4604,11 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
visibility: style.visibility,
overflow: style.overflow,
overflowY: style.overflowY,
overflowX: style.overflowX,
scrollHeight: element.scrollHeight,
clientWidth: element.clientWidth,
clientHeight: element.clientHeight,
scrollWidth: element.scrollWidth,
scrollHeight: element.scrollHeight,
text: element.textContent?.replace(/\s+/gu, " ").trim().slice(0, 120) ?? ""
};
};
@@ -4211,6 +4742,8 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
const shell = document.querySelector("[data-app-shell]");
const toggleElement = document.querySelector("#explorer-toggle");
const isDesktop = viewport.width >= 861;
const isMobile = !isDesktop;
const boxes = {
shell: boxFor("[data-app-shell]"),
rail: boxFor(".activity-rail"),
@@ -4228,6 +4761,26 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
controlList: boxFor("#control-list"),
topbar: boxFor(".topbar")
};
const noHorizontalOverflow = {
html: document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2,
body: document.body.scrollWidth <= document.body.clientWidth + 2,
shell: boxes.shell ? boxes.shell.scrollWidth <= boxes.shell.clientWidth + 2 : false,
right: boxes.right ? boxes.right.scrollWidth <= boxes.right.clientWidth + 2 : false,
hardwareList: (() => {
const element = document.querySelector("#hardware-list");
return element ? element.scrollWidth <= element.clientWidth + 2 : false;
})()
};
const hardwareTabRows = (() => {
const tabs = [...document.querySelectorAll("[data-hardware-tab]")];
return new Set(tabs.map((tab) => Math.round(tab.getBoundingClientRect().top))).size;
})();
const rightPanelStacked = Boolean(boxes.right && boxes.center && boxes.right.top >= boxes.center.bottom - 2);
const rightWidthTarget = isDesktop
? rightPanelStacked
? boxes.right?.width >= 560 && boxes.right?.width <= viewport.width
: boxes.right?.width >= (viewport.width >= 1500 ? 700 : 560) && boxes.right?.width <= 760
: boxes.right?.width <= viewport.width && boxes.right?.width >= Math.max(0, viewport.width - 80);
const rootAfterScrollAttempt = {
windowScrollY: window.scrollY,
htmlScrollTop: document.documentElement.scrollTop,
@@ -4259,8 +4812,6 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
boxes.explorer.width > 1 &&
boxes.explorer.height > 1
);
const isDesktop = viewport.width >= 861;
const isMobile = !isDesktop;
const requireKeyTargets = !(isMobile && mode === "mobile-drawer");
if (requireKeyTargets) {
for (const target of blockedKeyTargets) {
@@ -4564,6 +5115,16 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
sidePanelScroll
});
}
if (!rightWidthTarget) {
failures.push({
failureType: "overflow",
selector: ".right-sidebar",
viewport,
summary: "M3 hardware status workspace width is outside the expected desktop, stacked desktop, or mobile boundary.",
rightPanelStacked,
rightBox: boxes.right
});
}
const issue287HardwareTabsReady = ["总览", "Gateway-SIMU", "BOX-SIMU-1", "BOX-SIMU-2", "Patch Panel"].every((label) =>
Boolean([...document.querySelectorAll(".hardware-status button, .hardware-status [role='tab'], .hardware-status .side-tab")].find((item) => item.textContent?.includes(label)))
);
@@ -4580,6 +5141,8 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
noIncoherentOverlap &&
semanticOverlapChecks.every((check) => !check.overlaps) &&
overflowOk &&
Object.values(noHorizontalOverflow).every(Boolean) &&
rightWidthTarget &&
keyTargetsRequirement &&
collapsedStateMatches &&
explorerStateMatches &&
@@ -4606,7 +5169,12 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
rightWidthDelta,
collapsedWidthGain,
restoredWidth,
rightPanelStacked,
overlapChecks,
noHorizontalOverflow,
horizontalOverflowFree: Object.values(noHorizontalOverflow).every(Boolean),
hardwareTabRows,
rightWidthTarget,
noIncoherentOverlap,
keyTargetsReachable,
keyTargetsRequirement,
+6 -2
View File
@@ -333,6 +333,7 @@ export function checkFrontendNoDirectRuntimeCalls({ appSource, htmlSource = "",
"/v1",
"/v1/agent/chat",
"/v1/m3/io",
"/v1/m3/status",
"/json-rpc"
]);
@@ -361,8 +362,8 @@ export function checkFrontendNoDirectRuntimeCalls({ appSource, htmlSource = "",
if (!/fetchJson\("\/v1\/m3\/io"/u.test(actionBody)) {
issues.push("runM3IoAction must POST through same-origin /v1/m3/io");
}
if (/fetchJson\(["'`](?!\/v1\/m3\/io)/u.test(actionBody) || /\bfetch\(/u.test(actionBody)) {
issues.push("runM3IoAction must not call any path other than fetchJson(\"/v1/m3/io\")");
if (/fetchJson\(["'`](?!\/v1\/m3\/(?:io|status))/u.test(actionBody) || /\bfetch\(/u.test(actionBody)) {
issues.push("runM3IoAction must not call any path other than fetchJson(\"/v1/m3/io\") or fetchJson(\"/v1/m3/status\")");
}
if (!/if \(!m3ControlCanOperate\(\)\)/u.test(actionBody)) {
issues.push("runM3IoAction must fail closed before POST when M3 readiness is blocked");
@@ -381,6 +382,9 @@ export function checkFrontendNoDirectRuntimeCalls({ appSource, htmlSource = "",
if (!/url\.pathname === "\/v1\/m3\/io"/u.test(artifactPublisherSource)) {
issues.push("cloud-web artifact server must proxy same-origin /v1/m3/io to cloud-api");
}
if (!/\/v1\/m3\/status/u.test(artifactPublisherSource)) {
issues.push("cloud-web artifact server must proxy same-origin /v1/m3/status to cloud-api");
}
return {
status: issues.length === 0 ? "pass" : "fail",
+349 -118
View File
@@ -37,6 +37,7 @@ const STATUS_LABELS = Object.freeze({
sent: "已发送",
requires: "依赖",
source: "来源",
unverified: "未验证",
"dry-run": "演练",
"dev-live": "实况已验证"
});
@@ -111,6 +112,8 @@ const state = {
liveSurface: null,
m3Control: {
contract: null,
status: null,
hardwareTab: "overview",
operation: null,
pending: false
}
@@ -120,6 +123,7 @@ initRoutes();
initExplorerToggle();
syncMobileExplorer();
initSideTabs();
initHardwareTabs();
initQuickActions();
initCommandBar();
initM3Control();
@@ -277,6 +281,25 @@ function selectSideTab(tabId) {
}
}
function initHardwareTabs() {
for (const tab of document.querySelectorAll("[data-hardware-tab]")) {
tab.addEventListener("click", () => {
state.m3Control.hardwareTab = tab.dataset.hardwareTab;
syncHardwareTabs();
renderHardwareStatus(state.m3Control.status);
});
}
syncHardwareTabs();
}
function syncHardwareTabs() {
for (const tab of document.querySelectorAll("[data-hardware-tab]")) {
const active = tab.dataset.hardwareTab === state.m3Control.hardwareTab;
tab.classList.toggle("active", active);
tab.setAttribute("aria-selected", active ? "true" : "false");
}
}
function initCommandBar() {
el.commandForm.addEventListener("submit", async (event) => {
event.preventDefault();
@@ -466,10 +489,11 @@ async function sendAgentMessage(message, conversationId, traceId = nextProtocolI
async function loadLiveSurface() {
const projectId = gateSummary.topology.projectId;
const [healthLive, restIndex, m3Control, health, adapter, audit, evidence] = await Promise.all([
const [healthLive, restIndex, m3Control, m3Status, health, adapter, audit, evidence] = await Promise.all([
fetchJson("/health/live"),
fetchJson("/v1"),
fetchJson("/v1/m3/io"),
fetchJson("/v1/m3/status"),
callRpc("system.health"),
callRpc("cloud.adapter.describe"),
callRpc("audit.event.query", { projectId, limit: 6 }),
@@ -481,6 +505,7 @@ async function loadLiveSurface() {
healthLive,
restIndex,
m3Control,
m3Status,
health,
adapter,
audit,
@@ -666,10 +691,14 @@ async function runM3IoAction(action) {
sourceKind: response.data?.evidenceState?.status === "green" ? "DEV-LIVE" : "BLOCKED"
};
}
const refreshedStatus = await fetchJson("/v1/m3/status");
if (refreshedStatus.ok) {
state.m3Control.status = refreshedStatus.data;
}
state.m3Control.pending = false;
renderM3ControlStatus();
renderHardwareStatus(runtimeSummaryFrom(state.liveSurface));
renderWiringList(runtimeSummaryFrom(state.liveSurface));
renderHardwareStatus(state.m3Control.status);
renderWiringList(state.m3Control.status);
renderRecords(state.liveSurface);
renderDrafts();
}
@@ -682,6 +711,7 @@ function nextProtocolId(prefix) {
function renderLiveSurface(live) {
state.liveSurface = live;
state.m3Control.contract = live.m3Control?.ok ? live.m3Control.data : null;
state.m3Control.status = live.m3Status?.ok ? live.m3Status.data : null;
const coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter];
const surfaceStatus = workbenchApiSurfaceStatus(live, coreProbes);
const runtimeSummary = runtimeSummaryFrom(live);
@@ -693,8 +723,8 @@ function renderLiveSurface(live) {
el.liveDetail.textContent = surfaceStatus.detail ?? codeAgentAvailabilitySummary();
renderAgentChatStatus(deriveAgentChatStatus());
renderHardwareStatus(runtimeSummary);
renderWiringList(runtimeSummary);
renderHardwareStatus(state.m3Control.status);
renderWiringList(state.m3Control.status ?? runtimeSummary);
renderRecords(live);
renderDiagnostics(live);
renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, surfaceStatus.methodTone);
@@ -719,16 +749,16 @@ function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.res
label: "可信记录受阻 / 只读模式",
tone: "blocked",
methodTone: "degraded",
detail: "同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。"
detail: apiDurableBlockerDetail(coreProbes)
};
}
if (surface.degraded) {
return {
label: "API 降级 / 只读模式",
tone: "degraded",
label: "API 错误 / 只读模式",
tone: "blocked",
methodTone: "degraded",
detail: "同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。"
detail: apiProbeErrorDetail(coreProbes)
};
}
@@ -740,6 +770,33 @@ function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.res
};
}
function apiDurableBlockerDetail(coreProbes = []) {
const payloads = coreProbes
.filter((probe) => probe?.ok)
.map((probe) => probe.data)
.filter((payload) => payload && typeof payload === "object");
const blocker = safeDurableBlockerLabel(payloads.map(durableRuntimeBlockerCode).find(Boolean));
return `/health/live 可响应,但可信记录持久化尚未就绪:${blocker};当前只保留只读查看和本地任务整理。`;
}
function safeDurableBlockerLabel(blocker) {
const normalized = normalizedStatusValue(blocker);
if (normalized.includes("auth")) return "runtime durable adapter auth blocked";
if (normalized.includes("schema")) return "runtime durable adapter schema blocked";
if (normalized.includes("migration")) return "runtime durable adapter migration blocked";
if (normalized.includes("ssl")) return "runtime durable adapter ssl blocked";
if (normalized.includes("query")) return "runtime durable adapter query blocked";
return "runtime durable adapter blocked";
}
function apiProbeErrorDetail(coreProbes = []) {
const failedProbe = coreProbes.find((probe) => probe && probe.ok !== true);
if (failedProbe?.error?.message) {
return `同源 API 探测失败:${failedProbe.error.message};当前保持只读查看和本地任务整理。`;
}
return "同源 API 返回 blocked/degraded/failed 或 ready=false;当前保持只读查看和本地任务整理。";
}
function workbenchSurfaceReadiness(live, coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]) {
const payloads = coreProbes
.filter((probe) => probe?.ok)
@@ -775,6 +832,7 @@ function readinessBooleanValues(payload) {
}
function hasDurableRuntimeQueryBlock(payload) {
const legacyQueryBlockerCode = "runtime_durable_adapter_query_blocked";
const codes = [
...(Array.isArray(payload.blockerCodes) ? payload.blockerCodes : []),
...(Array.isArray(payload.readiness?.blockerCodes) ? payload.readiness.blockerCodes : []),
@@ -786,12 +844,34 @@ function hasDurableRuntimeQueryBlock(payload) {
const queryResult = normalizedStatusValue(payload.runtime?.connection?.queryResult ?? payload.readiness?.durability?.queryResult);
const blockedLayer = normalizedStatusValue(payload.runtime?.durabilityContract?.blockedLayer ?? payload.readiness?.durability?.blockedLayer);
return (
codes.includes("runtime_durable_adapter_query_blocked") ||
queryResult === "query_blocked" ||
blockedLayer === "durability_query"
codes.includes(legacyQueryBlockerCode) ||
codes.some((code) => code.startsWith("runtime_durable_adapter_") && code.endsWith("_blocked")) ||
queryResult.endsWith("_blocked") ||
blockedLayer === "durability_query" ||
blockedLayer === "durability_auth" ||
blockedLayer === "durability_schema" ||
blockedLayer === "durability_migration" ||
blockedLayer === "durability_ssl"
);
}
function durableRuntimeBlockerCode(payload) {
if (!payload || typeof payload !== "object") return null;
const candidates = [
...(Array.isArray(payload.blockerCodes) ? payload.blockerCodes : []),
...(Array.isArray(payload.readiness?.blockerCodes) ? payload.readiness.blockerCodes : []),
...(Array.isArray(payload.blockers) ? payload.blockers.map((item) => item?.code) : []),
...(Array.isArray(payload.readiness?.blockers) ? payload.readiness.blockers.map((item) => item?.code) : []),
payload.runtime?.blocker,
payload.readiness?.durability?.blocker,
payload.runtime?.connection?.queryResult,
payload.readiness?.durability?.queryResult,
payload.runtime?.durabilityContract?.blockedLayer,
payload.readiness?.durability?.blockedLayer
];
return candidates.find((value) => normalizedStatusValue(value).includes("durable") || normalizedStatusValue(value).endsWith("_blocked")) ?? null;
}
function normalizedStatusValue(value) {
return String(value ?? "").trim().toLowerCase();
}
@@ -948,108 +1028,31 @@ function renderUnblockList() {
replaceChildren(el.unblockList, ...rows.map(infoCard));
}
function renderHardwareStatus(summary) {
const counts = summary?.counts ?? null;
const patchPanel = gateSummary.topology.patchPanel;
const sourceLink = trustedM3Link(patchPanel.activeConnections);
const liveM3Evidence = trustedM3LiveEvidenceFrom(summary) ?? m3ControlLiveEvidence();
const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence);
const controlReady = m3ControlCanOperate();
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";
function renderHardwareStatus(m3Status) {
syncHardwareTabs();
const status = m3Status ?? sourceFallbackM3Status();
const sourceKind = status.sourceKind ?? "UNVERIFIED";
const trustedGreen = status.trust?.durableStatus === "green";
const statusTone = status.status === "live" && trustedGreen
? "dev-live"
: status.status === "error" || status.status === "blocked"
? "blocked"
: sourceKind === "DEV-LIVE"
? "dev-live"
: "source";
el.hardwareSourceStatus.textContent = m3Live ? "实况已验证" : "只读核对";
el.hardwareSourceStatus.className = `state-tag tone-${toneClass(m3Live ? "dev-live" : "source")}`;
el.hardwareSourceStatus.textContent = hardwareStatusLabel(status);
el.hardwareSourceStatus.className = `state-tag tone-${toneClass(statusTone)}`;
replaceChildren(
el.hardwareList,
hardwareGroup({
title: "Gateway/Box 在线",
rows: [
{
label: "Gateway-SIMU",
value: counts ? `${counts.gatewaySessions ?? 0} 会话` : "离线查看",
detail: counts
? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读运行计数;仅用于资源核对。`
: `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自内置拓扑。`,
sourceKind: runtimeCountsSourceKind,
tone: runtimeCountsTone
},
{
label: "BOX-SIMU",
value: counts ? `${counts.boxResources ?? 0} 资源` : "离线查看",
detail: counts
? `${counts.boxResources ?? 0} 个 BOX 资源来自只读运行计数;仅用于资源核对。`
: `${gateSummary.boxResourceCount} 个 BOX 资源来自内置拓扑。`,
sourceKind: runtimeCountsSourceKind,
tone: runtimeCountsTone
}
]
}),
hardwareGroup({
title: "BOX-SIMU 端口",
rows: [
{
label: "DO1",
value: m3Live ? "已验证" : "待接线",
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "尚未看到 DO1 电平的完整可信记录。",
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
tone: m3Live ? "live" : "blocked"
},
{
label: "DI1",
value: m3Live ? "已验证" : "待回读",
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "尚未看到 DI1 回读的完整可信记录。",
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
tone: m3Live ? "live" : "blocked"
},
{
label: "AI/AO/FREQ",
value: "后续能力",
detail: "当前未声明模拟量或频率闭环,按后续能力处理。",
sourceKind: "SOURCE",
tone: "disabled"
}
]
}),
hardwareGroup({
title: "Patch Panel 连线",
rows: [
{
label: "hwlab-patch-panel",
value: statusLabel(patchPanel.state),
detail: m3Live
? `接线盘实况记录 ${liveM3Evidence.patchPanelLiveReport.reportId ?? liveM3Evidence.patchPanelLiveReport.patchPanelStatusId} 已绑定目标链路。`
: `${patchPanel.activeConnectionCount} 条内置接线记录;工作台只展示目标链路,未升级为可信闭环证据。`,
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: controlReady ? "受控路径" : "受控路径受阻",
detail: controlReady
? "控制只通过受控后端执行,并经由 gateway-simu、box-simu 与 patch-panel;页面不直连模拟器,也不伪造硬件状态。"
: `${m3ControlBlockedReason()} 页面不直连模拟器,也不伪造硬件状态。`,
sourceKind: controlReady ? "SOURCE" : "BLOCKED",
tone: controlReady ? "source" : "blocked"
}
]
})
);
const groups = {
overview: hardwareOverviewGroups(status),
gateways: hardwareGatewayGroups(status),
box1: hardwareBoxGroups(status, M3_TRUSTED_ROUTE.fromResourceId),
box2: hardwareBoxGroups(status, M3_TRUSTED_ROUTE.toResourceId),
patch: hardwarePatchPanelGroups(status)
};
// Tab bodies are compact Key/Value groups; SOURCE fallback stays unverified.
replaceChildren(el.hardwareList, ...(groups[state.m3Control.hardwareTab] ?? groups.overview));
}
function hardwareGroup(group) {
@@ -1076,6 +1079,230 @@ function hardwareRow(item) {
return row;
}
function hardwareStatusLabel(status) {
if (!status) return "未验证";
if (status.status === "live" && status.trust?.durableStatus === "green") return "实况可信";
if (status.status === "error") return "错误";
if (status.status === "blocked") return "实况受阻";
if (status.sourceKind === "DEV-LIVE") return "DEV-LIVE 状态";
return "未验证";
}
function sourceFallbackM3Status() {
const observedAt = new Date().toISOString();
return {
status: "unverified",
sourceKind: "SOURCE",
observedAt,
chain: {
sourceGatewayId: "gwsimu_1",
sourceGatewaySessionId: "gws_gwsimu_1",
sourceResourceId: M3_TRUSTED_ROUTE.fromResourceId,
sourceBoxId: "boxsimu_1",
sourcePort: M3_TRUSTED_ROUTE.fromPort,
targetGatewayId: "gwsimu_2",
targetGatewaySessionId: "gws_gwsimu_2",
targetResourceId: M3_TRUSTED_ROUTE.toResourceId,
targetBoxId: "boxsimu_2",
targetPort: M3_TRUSTED_ROUTE.toPort,
patchPanelServiceId: M3_TRUSTED_ROUTE.patchPanelServiceId
},
gateways: [
{
id: "gwsimu_1",
role: "source",
online: false,
sessionId: "gws_gwsimu_1",
sourceKind: "SOURCE",
lastSeenAt: observedAt,
lastError: "等待 cloud-api /v1/m3/status 聚合读取"
},
{
id: "gwsimu_2",
role: "target",
online: false,
sessionId: "gws_gwsimu_2",
sourceKind: "SOURCE",
lastSeenAt: observedAt,
lastError: "等待 cloud-api /v1/m3/status 聚合读取"
}
],
boxes: [
sourceFallbackBox("boxsimu_1", M3_TRUSTED_ROUTE.fromResourceId, "gwsimu_1", "gws_gwsimu_1", "DO1", "output", observedAt),
sourceFallbackBox("boxsimu_2", M3_TRUSTED_ROUTE.toResourceId, "gwsimu_2", "gws_gwsimu_2", "DI1", "input", observedAt)
],
patchPanel: {
serviceId: M3_TRUSTED_ROUTE.patchPanelServiceId,
connectionActive: false,
connectionVersion: null,
wiringConfigId: gateSummary.topology.patchPanel.wiringConfigId ?? null,
lastSyncAt: observedAt,
lastError: "SOURCE 拓扑 fallback,未验证 DEV-LIVE 接线盘状态",
sourceKind: "SOURCE"
},
trust: {
operationId: null,
traceId: null,
auditId: null,
evidenceId: null,
durableStatus: "blocked",
blocker: "unverified_cloud_api_m3_status"
},
blocker: {
code: "unverified_cloud_api_m3_status",
layer: "cloud-api",
zh: "等待 cloud-api /v1/m3/status 聚合读取;SOURCE 拓扑不能冒充实况硬件状态。"
}
};
}
function sourceFallbackBox(id, resourceId, gatewayId, gatewaySessionId, port, direction, observedAt) {
return {
id,
resourceId,
gatewayId,
gatewaySessionId,
online: false,
sourceKind: "SOURCE",
observedAt,
ports: {
[port]: {
value: null,
direction,
source: "SOURCE fallback",
sourceKind: "SOURCE",
observedAt,
lastError: "未验证"
}
}
};
}
function hardwareOverviewGroups(status) {
const box1 = findM3Box(status, M3_TRUSTED_ROUTE.fromResourceId);
const box2 = findM3Box(status, M3_TRUSTED_ROUTE.toResourceId);
const do1 = box1?.ports?.DO1;
const di1 = box2?.ports?.DI1;
const patch = status.patchPanel ?? {};
const trust = status.trust ?? {};
return [
hardwareKvGroup("总览", [
["状态", hardwareStatusLabel(status), statusTone(status)],
["观测时间", status.observedAt ?? "未验证", "source"],
["Gateway 在线", `${onlineCount(status.gateways)} / ${status.gateways?.length ?? 0}`, onlineCount(status.gateways) === 2 ? "live" : "blocked"],
["BOX 在线", `${onlineCount(status.boxes)} / ${status.boxes?.length ?? 0}`, onlineCount(status.boxes) === 2 ? "live" : "blocked"],
["链路", `${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort}`, patch.connectionActive ? "live" : "blocked"],
["DO1", valueLabel(do1), portTone(do1)],
["DI1", valueLabel(di1), portTone(di1)],
["patchPanel connectionActive", String(Boolean(patch.connectionActive)), patch.connectionActive ? "live" : "blocked"],
["runtime durable", trust.durableStatus ?? "blocked", trust.durableStatus === "green" ? "dev-live" : "blocked"],
["blocker", trust.blocker ?? status.blocker?.code ?? "none", trust.blocker || status.blocker ? "blocked" : "source"]
]),
hardwareKvGroup("可信证据", [
["operationId", trust.operationId ?? "未持久化", trust.operationId ? "source" : "blocked"],
["traceId", trust.traceId ?? "未持久化", trust.traceId ? "source" : "blocked"],
["auditId", trust.auditId ?? "未持久化", trust.auditId ? "source" : "blocked"],
["evidenceId", trust.evidenceId ?? "未持久化", trust.evidenceId ? "source" : "blocked"]
])
];
}
function hardwareGatewayGroups(status) {
return (status.gateways ?? []).map((gateway) =>
hardwareKvGroup(`${gateway.id} / ${gateway.role}`, [
["online", String(Boolean(gateway.online)), gateway.online ? "live" : "blocked"],
["sessionId", gateway.sessionId ?? "未验证", gateway.sessionId ? "source" : "blocked"],
["lastSeenAt", gateway.lastSeenAt ?? status.observedAt ?? "未验证", gateway.online ? "live" : "source"],
["source", gateway.sourceKind ?? "UNVERIFIED", sourceKindTone(gateway.sourceKind)],
["lastError", gateway.lastError ?? "none", gateway.lastError ? "blocked" : "source"]
])
);
}
function hardwareBoxGroups(status, resourceId) {
const box = findM3Box(status, resourceId);
const portEntries = Object.entries(box?.ports ?? {});
return [
hardwareKvGroup(box ? `${box.id} / ${box.resourceId}` : resourceId, [
["online", String(Boolean(box?.online)), box?.online ? "live" : "blocked"],
["gateway", `${box?.gatewayId ?? "未验证"} / ${box?.gatewaySessionId ?? "未验证"}`, box?.gatewayId ? "source" : "blocked"],
["更新时间", box?.observedAt ?? status.observedAt ?? "未验证", sourceKindTone(box?.sourceKind)],
["来源", box?.sourceKind ?? "UNVERIFIED", sourceKindTone(box?.sourceKind)],
...portEntries.map(([port, portState]) => [port, valueLabel(portState), portTone(portState)]),
["AI1", "DISABLED 后续能力", "disabled"],
["AO1", "DISABLED 后续能力", "disabled"],
["FREQ", "DISABLED 后续能力", "disabled"]
])
];
}
function hardwarePatchPanelGroups(status) {
const patch = status.patchPanel ?? {};
return [
hardwareKvGroup("hwlab-patch-panel", [
["serviceId", patch.serviceId ?? M3_TRUSTED_ROUTE.patchPanelServiceId, "source"],
["connectionActive", String(Boolean(patch.connectionActive)), patch.connectionActive ? "live" : "blocked"],
["connectionVersion", patch.connectionVersion ?? "未验证", patch.connectionVersion !== null && patch.connectionVersion !== undefined ? "source" : "blocked"],
["wiringConfigId", patch.wiringConfigId ?? "未验证", patch.wiringConfigId ? "source" : "blocked"],
["lastSyncAt", patch.lastSyncAt ?? status.observedAt ?? "未验证", patch.connectionActive ? "live" : "source"],
["lastError", patch.lastError ?? "none", patch.lastError ? "blocked" : "source"],
["来源", patch.sourceKind ?? "UNVERIFIED", sourceKindTone(patch.sourceKind)]
])
];
}
function hardwareKvGroup(title, rows) {
const article = document.createElement("article");
article.className = "hardware-section hardware-kv-section";
article.append(textSpan(title, "hardware-section-title"));
const list = document.createElement("dl");
list.className = "kv-list";
for (const [key, value, tone = "source"] of rows) {
const row = document.createElement("div");
row.className = `kv-row tone-border-${toneClass(tone)}`;
row.append(textSpan(key, "kv-key"), textSpan(value, `kv-value tone-${toneClass(tone)}`));
list.append(row);
}
article.append(list);
return article;
}
function findM3Box(status, resourceId) {
return (status?.boxes ?? []).find((box) => box.resourceId === resourceId) ?? null;
}
function onlineCount(items = []) {
return items.filter((item) => item?.online === true).length;
}
function valueLabel(portState) {
if (!portState) return "未验证";
if (portState.value === null || portState.value === undefined) return portState.lastError ? `未验证:${portState.lastError}` : "未验证";
return String(portState.value);
}
function portTone(portState) {
if (!portState) return "blocked";
if (portState.sourceKind === "DEV-LIVE") return "live";
if (portState.sourceKind === "SOURCE") return "source";
return "blocked";
}
function statusTone(status) {
if (status.status === "live") return "dev-live";
if (status.status === "error" || status.status === "blocked") return "blocked";
if (status.sourceKind === "DEV-LIVE") return "live";
return "source";
}
function sourceKindTone(sourceKind) {
const normalized = String(sourceKind ?? "").toUpperCase();
if (normalized === "DEV-LIVE") return "live";
if (normalized === "SOURCE") return "source";
if (normalized === "BLOCKED") return "blocked";
return "blocked";
}
function trustedM3Link(connections) {
return (connections ?? []).find(
(link) =>
@@ -1316,7 +1543,7 @@ function controlRows() {
title: "控制路径",
detail: controlReady
? "按钮只走受控后端,再通过 gateway-simu -> box-simu -> hwlab-patch-panel 执行 M3 DO/DI。"
: `${m3ControlBlockedReason()} 不可用时不会直连 gateway/box-simu,也不伪造硬件状态。`,
: `${m3ControlBlockedReason()} 不可用时页面不直连模拟器,也不伪造硬件状态。`,
tone: controlReady ? "source" : "blocked"
},
...operationCards,
@@ -1384,14 +1611,18 @@ function renderDrafts() {
}
function renderWiringList(summary = null) {
const patchPanel = gateSummary.topology.patchPanel;
const liveM3Evidence = trustedM3LiveEvidenceFrom(summary);
const m3Status = summary?.contractVersion === "m3-status-v1" ? summary : null;
const liveM3Evidence = m3Status ? null : trustedM3LiveEvidenceFrom(summary);
const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence);
const connectionActive = m3Status?.patchPanel?.connectionActive === true;
const blockedTrust = m3Status?.trust?.blocker;
const traceEvidence = m3Live
? [liveM3Evidence.operationId, liveM3Evidence.traceId, liveM3Evidence.auditId, liveM3Evidence.evidenceId].join(" / ")
: "等待可信记录";
const stateLabel = m3Live ? "实况已验证" : "待可信记录";
const sourceLabel = m3Live ? "实况可信记录" : "来源拓扑";
: m3Status
? [m3Status.trust?.operationId, m3Status.trust?.traceId, m3Status.trust?.auditId, m3Status.trust?.evidenceId].filter(Boolean).join(" / ") || `durable blocked: ${blockedTrust ?? "unverified"}`
: "等待可信记录";
const stateLabel = m3Live ? "实况已验证" : connectionActive ? "接线实况 / 持久化受阻" : "待可信记录";
const sourceLabel = m3Live ? "实况可信记录" : m3Status ? "/v1/m3/status 聚合" : "来源拓扑";
const sourceHeader = byId("wiring-source-device");
const targetHeader = byId("wiring-target-device");
+16 -30
View File
@@ -208,40 +208,26 @@
<section class="hardware-status">
<div class="panel-title-row">
<div>
<p class="eyebrow">运行界面</p>
<h2>硬件状态BOX-SIMU / Gateway-SIMU / hwlab-patch-panel</h2>
<p class="eyebrow">M3 实况状态</p>
<h2>硬件状态工作区</h2>
</div>
<span class="state-tag tone-source" id="hardware-source-status">只读核对</span>
<span class="state-tag tone-source" id="hardware-source-status">未验证</span>
</div>
<div class="hardware-status-tabs" role="tablist" aria-label="M3 硬件状态">
<button class="hardware-tab active" type="button" role="tab" aria-selected="true" data-hardware-tab="overview">总览</button>
<button class="hardware-tab" type="button" role="tab" aria-selected="false" data-hardware-tab="gateways">Gateway-SIMU</button>
<button class="hardware-tab" type="button" role="tab" aria-selected="false" data-hardware-tab="box1">BOX-SIMU-1</button>
<button class="hardware-tab" type="button" role="tab" aria-selected="false" data-hardware-tab="box2">BOX-SIMU-2</button>
<button class="hardware-tab" type="button" role="tab" aria-selected="false" data-hardware-tab="patch">Patch Panel</button>
</div>
<div id="hardware-list" class="hardware-list" data-static-source-fallback="SOURCE">
<article class="hardware-section">
<span class="hardware-section-title">只读资源概览</span>
<div class="hardware-rows">
<div class="hardware-row tone-border-source" data-source-kind="SOURCE">
<div class="hardware-row-head">
<span class="hardware-row-label">Gateway/Box 资源</span>
<span class="state-tag tone-source">来源拓扑</span>
</div>
<span class="hardware-row-value">离线查看</span>
<span class="hardware-row-detail">等待同源只读数据覆盖;当前展示内置拓扑摘要。</span>
</div>
<div class="hardware-row tone-border-source" data-source-kind="SOURCE">
<div class="hardware-row-head">
<span class="hardware-row-label">hwlab-patch-panel</span>
<span class="state-tag tone-source">来源拓扑</span>
</div>
<span class="hardware-row-value">目标接线</span>
<span class="hardware-row-detail">res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 仅为源码兜底显示。</span>
</div>
<div class="hardware-row tone-border-blocked" data-source-kind="BLOCKED">
<div class="hardware-row-head">
<span class="hardware-row-label">DO1 -> patch-panel -> DI1</span>
<span class="state-tag tone-blocked">待可信记录</span>
</div>
<span class="hardware-row-value">等待可信记录</span>
<span class="hardware-row-detail">尚未看到完整可信记录,不能作为实况闭环证据。</span>
</div>
</div>
<span class="hardware-section-title">Key / Value</span>
<dl class="kv-list">
<div><dt>数据入口</dt><dd>等待 cloud-api /v1/m3/status</dd></div>
<div><dt>链路</dt><dd>res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1</dd></div>
<div><dt>来源</dt><dd>未验证;拓扑资料只可作为 fallback</dd></div>
</dl>
</article>
</div>
</section>
+41 -22
View File
@@ -141,11 +141,13 @@ for (const legacyEnglishTitle of [
]) {
assert.doesNotMatch(`${html}\n${app}`, legacyEnglishTitle);
}
assert.match(html, /硬件状态:BOX-SIMU \/ Gateway-SIMU \/ hwlab-patch-panel/);
assert.match(html, /M3 实况状态/);
assert.match(html, /硬件状态工作区/);
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 initHardwareTabs/);
assert.match(app, /function hardwareKvGroup/);
assert.match(app, /function sourceFallbackM3Status/);
assert.match(app, /function trustedM3Link/);
assert.match(app, /function hasTrustedM3LiveEvidence/);
assert.match(app, /function hasTrustedPatchPanelLiveReport/);
@@ -153,9 +155,10 @@ 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.match(functionBody(app, "sourceFallbackM3Status"), /unverified_cloud_api_m3_status/);
assert.match(functionBody(app, "sourceFallbackM3Status"), /SOURCE 拓扑不能冒充实况硬件状态/);
assert.doesNotMatch(functionBody(app, "renderHardwareStatus"), /hardwareRow\(\s*\{/u);
assert.match(functionBody(app, "hardwareGroup"), /group\.rows\.map\(hardwareRow\)/u);
assert.match(functionBody(app, "renderHardwareStatus"), /Key\/Value groups/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"/);
assert.match(app, /function renderRecords/);
@@ -186,28 +189,29 @@ assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*150000/);
assert.match(app, /async function sendAgentMessage[\s\S]*?timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/);
assert.match(functionBody(app, "fetchJson"), /timeoutMs\s*=\s*API_TIMEOUT_MS/);
for (const hardwareTerm of [
"Gateway/Box 在线",
"BOX-SIMU 端口",
"Patch Panel 连线",
"DO1 -> patch-panel -> DI1",
"AI/AO/FREQ",
"后续能力",
"Gateway 在线",
"BOX 在线",
"patchPanel connectionActive",
"runtime durable",
"DISABLED 后续能力",
"SOURCE",
"DEV-LIVE",
"BLOCKED",
"只读运行计数;仅用于资源核对",
"只读运行计数已返回,但还不能证明接线盘闭环",
"unverified_cloud_api_m3_status",
"/v1/m3/status",
"仍在等待完整可信记录",
"页面不直连模拟器,也不伪造硬件状态。"
]) {
assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`);
}
for (const hardwareHtmlTerm of ["Gateway-SIMU", "BOX-SIMU-1", "BOX-SIMU-2", "Patch Panel"]) {
assert.match(html, new RegExp(escapeRegExp(hardwareHtmlTerm)), `missing hardware HTML term: ${hardwareHtmlTerm}`);
}
assert.match(app, /function hardwareEvidenceLabel/);
assert.match(app, /dataset\.sourceKind/);
assert.match(styles, /\.record-group\s*{/);
assert.match(styles, /\.record-group-title\s*{/);
assert.match(styles, /\.hardware-section\s*{/);
assert.match(styles, /\.hardware-row\s*{/);
assert.match(styles, /\.hardware-status-tabs\s*{/);
assert.match(styles, /\.kv-row\s*{/);
assert.match(html, /Gate \/ 诊断 \/ 验收/);
assert.match(app, /new Set\(\["\/gate", "\/diagnostics\/gate"\]\)/);
assert.match(html, /href="\/styles\.css"/);
@@ -240,9 +244,9 @@ assert.match(html, />收起资源树<\/button>/);
assert.match(app, /setAttribute\("aria-label", collapsed \? "展开左侧资源树" : "收起左侧资源树"\)/);
assert.match(app, /textContent = collapsed \? "展开资源树" : "收起资源树"/);
assert.match(styles, /--explorer-width:\s*292px/);
assert.match(styles, /--right-width:\s*clamp\(366px,\s*28vw,\s*440px\)/);
assert.match(styles, /--right-width-expanded:\s*clamp\(366px,\s*26vw,\s*410px\)/);
assert.match(styles, /\.explorer-collapsed\s*\{[^}]*--explorer-width:\s*0px;[^}]*grid-template-columns:\s*var\(--rail-width\)\s+0\s+minmax\(560px,\s*1fr\)\s+var\(--right-width\);/s);
assert.match(styles, /--right-width:\s*clamp\(560px,\s*38vw,\s*740px\)/);
assert.match(styles, /--right-width-expanded:\s*clamp\(560px,\s*45vw,\s*740px\)/);
assert.match(styles, /\.explorer-collapsed\s*\{[^}]*--explorer-width:\s*0px;[^}]*grid-template-columns:\s*var\(--rail-width\)\s+0\s+minmax\(420px,\s*1fr\)\s+var\(--right-width\);/s);
for (const viewId of ["workspace", "gate"]) {
assert.match(html, new RegExp(`data-view="${viewId}"`));
}
@@ -345,13 +349,15 @@ assert.match(functionBody(app, "readinessBooleanValues"), /payload\.ready/);
assert.match(functionBody(app, "readinessBooleanValues"), /payload\.readiness\?\.ready/);
assert.match(functionBody(app, "workbenchSurfaceReadiness"), /readyValues\.includes\(false\)/);
assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /runtime_durable_adapter_query_blocked/);
assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /runtime_durable_adapter_/);
assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /payload\.runtime\?\.blocker/);
assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /payload\.readiness\?\.durability\?\.blocker/);
assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /blockedLayer === "durability_query"/);
assert.match(app, /可信记录受阻 \/ 只读模式/);
assert.match(app, /API 降级 \/ 只读模式/);
assert.match(app, /同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。/);
assert.match(app, /同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。/);
assert.match(app, /API 错误 \/ 只读模式/);
assert.match(app, /同源 API 返回 blocked\/degraded\/failed 或 ready=false;当前保持只读查看和本地任务整理。/);
assert.match(app, /\/health\/live 可响应,但可信记录持久化尚未就绪/);
assert.doesNotMatch(app, /API 降级 \/ 只读模式/);
assert.doesNotMatch(functionBody(app, "workbenchApiSurfaceStatus"), /runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u);
assert.match(html, /method-list/);
assert.match(html, /quick-actions/);
@@ -365,6 +371,7 @@ assert.doesNotMatch(html, /主流程[\s\S]*添加草稿/u);
assert.match(app, /sendAgentMessage/);
assert.match(app, /fetchJson\("\/v1\/agent\/chat"/);
assert.match(app, /fetchJson\("\/v1\/m3\/io"/);
assert.match(app, /fetchJson\("\/v1\/m3\/status"\)/);
assert.match(html, /id="m3-control-form"/);
assert.match(html, /id="m3-control-status"/);
assert.match(html, />写入 DO1<\/button>/);
@@ -375,6 +382,17 @@ assert.match(app, /frontendBypass=false/);
assert.match(app, /按钮只走受控后端/);
assert.match(app, /页面不直连模拟器,也不伪造硬件状态。/);
assert.match(styles, /\.m3-control-form\s*{/);
assert.match(html, /data-hardware-tab="overview"[\s\S]*>总览<\/button>/);
assert.match(html, /data-hardware-tab="gateways"[\s\S]*>Gateway-SIMU<\/button>/);
assert.match(html, /data-hardware-tab="box1"[\s\S]*>BOX-SIMU-1<\/button>/);
assert.match(html, /data-hardware-tab="box2"[\s\S]*>BOX-SIMU-2<\/button>/);
assert.match(html, /data-hardware-tab="patch"[\s\S]*>Patch Panel<\/button>/);
assert.match(app, /function initHardwareTabs/);
assert.match(app, /function hardwareKvGroup/);
assert.match(app, /sourceFallbackM3Status/);
assert.match(styles, /\.hardware-status-tabs\s*\{/);
assert.match(styles, /\.kv-list\s*\{/);
assert.match(styles, /\.right-sidebar \.table-wrap\s*\{[^}]*overflow-x:\s*hidden;/s);
assert.match(app, /Code Agent 调用失败/);
assert.match(app, /normalizeBlockedAgentResult/);
assert.match(app, /isBlockedAgentResponse/);
@@ -491,6 +509,7 @@ assert.match(artifactPublisher, /from "node:http"/);
assert.doesNotMatch(artifactPublisher, /await fetch\(target/);
assert.match(artifactPublisher, /url\.pathname === "\/v1\/agent\/chat"/);
assert.match(artifactPublisher, /url\.pathname === "\/v1\/m3\/io"/);
assert.match(artifactPublisher, /url\.pathname === "\/v1\/m3\/status"/);
assert.match(artifactPublisher, /"system\.health"/);
assert.match(artifactPublisher, /"cloud\.adapter\.describe"/);
assert.match(artifactPublisher, /"audit\.event\.query"/);
@@ -5,7 +5,8 @@ import { fileURLToPath } from "node:url";
import {
M3_IO_CHAIN,
M3_IO_CONTROL_ROUTE
M3_IO_CONTROL_ROUTE,
M3_STATUS_ROUTE
} from "../../../internal/cloud/m3-io-control.mjs";
import { checkFrontendNoDirectRuntimeCalls } from "../../../scripts/src/m3-io-control-e2e.mjs";
@@ -31,6 +32,7 @@ export function runM3ControlPanelGuard() {
const browserSource = `${html}\n${app}\n${styles}`;
assert.equal(M3_IO_CONTROL_ROUTE, "/v1/m3/io", "M3 IO control route must remain exact");
assert.equal(M3_STATUS_ROUTE, "/v1/m3/status", "M3 status aggregate route must remain exact");
assert.equal(
exactTrustedRoute,
"res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
@@ -57,7 +59,7 @@ export function runM3ControlPanelGuard() {
"M3 IO 只读 readiness 已确认",
"受控路径受阻",
"等待 cloud-api 返回 M3 IO 只读 readiness",
"不可用时不会直连 gateway/box-simu",
"不可用时页面不直连模拟器",
"浏览器不直连 box-simu",
"阻塞原因=",
"仍在等待完整可信记录",
@@ -118,8 +120,9 @@ function assertControlPanelSource(app) {
const actionBody = functionBody(app, "runM3IoAction");
assert.match(actionBody, /if \(!m3ControlCanOperate\(\)\)/u, "M3 action must fail closed before POST when readiness is blocked");
assert.match(actionBody, /fetchJson\("\/v1\/m3\/io",\s*\{[\s\S]*method:\s*"POST"/u, "M3 action must POST same-origin /v1/m3/io");
assert.match(actionBody, /fetchJson\("\/v1\/m3\/status"\)/u, "M3 action may refresh only through same-origin aggregate status after control");
assert.doesNotMatch(actionBody, /\bfetch\(/u, "M3 action must not use raw fetch");
assert.doesNotMatch(actionBody, /fetchJson\(\s*["'`](?!\/v1\/m3\/io)/u, "M3 action must not call any route except /v1/m3/io");
assert.doesNotMatch(actionBody, /fetchJson\(\s*["'`](?!\/v1\/m3\/(?:io|status))/u, "M3 action must not call any route except /v1/m3/io and /v1/m3/status");
assert.match(actionBody, /gatewayId:\s*el\.m3GatewaySelect\.value/u, "DO write request must use the selected source gateway");
assert.match(actionBody, /resourceId:\s*el\.m3BoxSelect\.value/u, "DO write request must use the selected source resource");
assert.match(actionBody, /boxId:\s*"boxsimu_1"/u, "DO write request must target source boxsimu_1");
@@ -169,6 +172,7 @@ function assertControlPanelSource(app) {
function assertBrowserWriteBoundaries({ app, html, artifactPublisher }) {
const browserSource = `${html}\n${app}`;
const sanitizedBrowserSource = browserSource.replace(/data-static-source-fallback="SOURCE"/gu, "");
const frontendGuard = checkFrontendNoDirectRuntimeCalls({
appSource: app,
htmlSource: html,
@@ -191,6 +195,7 @@ function assertBrowserWriteBoundaries({ app, html, artifactPublisher }) {
const controlWriteTargets = postTargets.filter((target) => target !== "/json-rpc" && target !== "/v1/agent/chat");
assert.deepEqual(controlWriteTargets, [M3_IO_CONTROL_ROUTE], "same-origin /v1/m3/io is the only browser hardware write control route");
assert.match(app, /fetchJson\("\/v1\/m3\/status"\)/u, "browser hardware status reads must use same-origin /v1/m3/status");
for (const [pattern, label] of [
[/\bcallRpc\(\s*["']hardware\./u, "generic hardware JSON-RPC"],
@@ -203,7 +208,7 @@ function assertBrowserWriteBoundaries({ app, html, artifactPublisher }) {
[/https?:\/\/[^"'`\s]*(?:gateway-simu|box-simu|patch-panel|:7101|:7201|:7301)/iu, "direct simulator URL"],
[/\/wiring\/apply|\/wiring\/reload|\/signals\/route/u, "patch-panel write endpoint"]
]) {
assert.doesNotMatch(browserSource, pattern, `browser source must not expose ${label}`);
assert.doesNotMatch(sanitizedBrowserSource, pattern, `browser source must not expose ${label}`);
}
}
@@ -14,6 +14,7 @@ const READ_ONLY_RPC_METHODS = Object.freeze([
"evidence.record.query"
]);
const CONTROL_ROUTE = "/v1/m3/io";
const STATUS_ROUTE = "/v1/m3/status";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = path.resolve(rootDir, "../..");
@@ -58,6 +59,7 @@ export function runCloudWebM3ReadonlyContract() {
);
assert.match(app, /fetchJson\("\/v1"\)/u, "Cloud Web must read the public REST index");
assert.match(app, /fetchJson\("\/v1\/m3\/io"/u, "Cloud Web M3 control must use only the same-origin cloud-api M3 IO route");
assert.match(app, /fetchJson\("\/v1\/m3\/status"\)/u, "Cloud Web M3 status must use only the same-origin cloud-api aggregate status route");
assert.match(app, /fetchJson\("\/json-rpc"/u, "Cloud Web must use same-origin JSON-RPC for diagnostics");
assert.match(app, /runtime\.serviceRoute\.join\(" -> "\)/u, "Cloud Web must render the observed route contract");
assert.ok(app.includes("frontendBypass=false"), "M3 control evidence must explicitly record no frontend bypass");
@@ -82,7 +84,7 @@ export function runCloudWebM3ReadonlyContract() {
const mutationFetches = [...app.matchAll(/fetchJson\(\s*["']([^"']+)["'][\s\S]{0,220}?method:\s*["']POST["']/gu)]
.map((match) => match[1])
.filter((path) => path !== "/v1/agent/chat" && path !== CONTROL_ROUTE && path !== "/json-rpc");
.filter((path) => path !== "/v1/agent/chat" && path !== CONTROL_ROUTE && path !== STATUS_ROUTE && path !== "/json-rpc");
assert.deepEqual(mutationFetches, [], "Cloud Web must not add POST mutation fetches outside Code Agent chat and M3 cloud-api control route");
const m3Milestone = gateSummary.milestones.find((item) => item.id === "M3");
+102 -6
View File
@@ -19,8 +19,8 @@
--info: #7ea6c9;
--rail-width: 92px;
--explorer-width: 292px;
--right-width: clamp(366px, 28vw, 440px);
--right-width-expanded: clamp(366px, 26vw, 410px);
--right-width: clamp(560px, 38vw, 740px);
--right-width-expanded: clamp(560px, 45vw, 740px);
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--body: "Aptos", "Segoe UI", system-ui, sans-serif;
}
@@ -76,7 +76,7 @@ ul {
max-height: 100dvh;
min-height: 0;
display: grid;
grid-template-columns: var(--rail-width) var(--explorer-width) minmax(460px, 1fr) var(--right-width-expanded);
grid-template-columns: var(--rail-width) var(--explorer-width) minmax(360px, 1fr) var(--right-width-expanded);
grid-template-rows: 1fr;
overflow: hidden;
overscroll-behavior: contain;
@@ -151,7 +151,7 @@ ul {
.explorer-collapsed {
--explorer-width: 0px;
grid-template-columns: var(--rail-width) 0 minmax(560px, 1fr) var(--right-width);
grid-template-columns: var(--rail-width) 0 minmax(420px, 1fr) var(--right-width);
}
.explorer-collapsed .explorer {
@@ -779,7 +779,7 @@ h3 {
.right-sidebar {
display: grid;
grid-template-rows: minmax(0, 0.45fr) minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) minmax(0, 0.9fr);
gap: 10px;
padding: 10px;
background: rgba(18, 20, 18, 0.98);
@@ -790,16 +790,53 @@ h3 {
.hardware-status {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
grid-template-rows: auto auto minmax(0, 1fr);
gap: 10px;
padding: 12px;
overflow: hidden;
}
.hardware-status-tabs {
min-width: 0;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.hardware-tab {
flex: 1 1 92px;
min-width: 0;
min-height: 30px;
padding: 5px 7px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
font-weight: 760;
cursor: pointer;
overflow-wrap: anywhere;
word-break: keep-all;
}
.hardware-tab:hover,
.hardware-tab:focus-visible,
.hardware-tab.active {
border-color: var(--line-strong);
background: var(--surface-2);
color: var(--text);
outline: 0;
}
.hardware-tab.active {
box-shadow: inset 0 -3px 0 var(--accent);
}
.hardware-list {
display: grid;
gap: 8px;
align-content: start;
overflow-x: hidden;
}
.hardware-section {
@@ -824,6 +861,49 @@ h3 {
gap: 6px;
}
.hardware-kv-section {
min-width: 0;
}
.kv-list {
min-width: 0;
display: grid;
gap: 6px;
margin: 0;
}
.kv-row {
min-width: 0;
display: grid;
grid-template-columns: minmax(96px, 0.42fr) minmax(0, 1fr);
gap: 8px;
align-items: start;
padding: 7px 8px;
background: var(--surface);
border: 1px solid var(--line);
border-left-width: 3px;
}
.kv-key,
.kv-value {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.kv-key {
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
font-weight: 760;
}
.kv-value {
color: var(--text);
font-family: var(--mono);
font-size: 11px;
}
.hardware-row {
min-width: 0;
display: grid;
@@ -931,6 +1011,7 @@ h3 {
gap: 10px;
padding: 12px;
overflow: auto;
overflow-x: hidden;
overscroll-behavior: contain;
}
@@ -1071,6 +1152,10 @@ h3 {
overflow-y: auto;
}
.right-sidebar .table-wrap {
overflow-x: hidden;
}
table {
width: 100%;
border-collapse: collapse;
@@ -1136,6 +1221,12 @@ table {
overflow-wrap: anywhere;
}
.right-sidebar table,
.right-sidebar .wiring-table {
min-width: 0;
table-layout: fixed;
}
th,
td {
text-align: left;
@@ -1428,6 +1519,11 @@ tbody tr:last-child td {
grid-template-columns: 1fr;
}
.kv-row {
grid-template-columns: 1fr;
gap: 3px;
}
.message-card,
.trace-item {
grid-template-columns: 1fr;