const SOURCE_KIND_ORDER = Object.freeze(["SOURCE", "DRY-RUN", "DEV-LIVE", "BLOCKED"]); export const M3_TRUSTED_CHAIN = Object.freeze({ fromResourceId: "res_boxsimu_1", fromPort: "DO1", patchPanelServiceId: "hwlab-patch-panel", toResourceId: "res_boxsimu_2", toPort: "DI1" }); export const hardwareEvidencePanelSourceLegend = Object.freeze([ { sourceKind: "SOURCE", label: "SOURCE", detail: "Static contract, topology, or checked-in source material." }, { sourceKind: "DRY-RUN", label: "DRY-RUN", detail: "Fixture or local validation that never touched the live hardware path." }, { sourceKind: "DEV-LIVE", label: "DEV-LIVE", detail: "Trusted DEV observation from the M3 closed loop." }, { sourceKind: "BLOCKED", label: "BLOCKED", detail: "Cannot promote to trusted evidence until DEV-LIVE M3 exists." } ]); export const defaultHardwareEvidencePanelData = Object.freeze({ title: "Hardware Evidence", subtitle: "Right-side workbench panel for the M3 trusted loop", sourceKind: "SOURCE", observedAt: null, statuses: [ { id: "res_boxsimu_1", label: "BOX-SIMU 1", kind: "BOX-SIMU", role: "source DO1", status: "degraded", sourceKind: "BLOCKED", detail: "No DEV-LIVE DO1 transition evidence is attached yet." }, { id: "gateway_simu_1", label: "Gateway-SIMU", kind: "Gateway-SIMU", role: "read-only route", status: "degraded", sourceKind: "SOURCE", detail: "Gateway shape is visible, but the M3 live loop is not proven." }, { id: "hwlab-patch-panel", label: "Patch Panel", kind: "Patch Panel", role: "wiring authority", status: "blocked", sourceKind: "BLOCKED", detail: "Waiting for a DEV-LIVE active link through hwlab-patch-panel." }, { id: "res_boxsimu_2", label: "BOX-SIMU 2", kind: "BOX-SIMU", role: "target DI1", status: "degraded", sourceKind: "BLOCKED", detail: "No DEV-LIVE DI1 observation is attached yet." } ], chain: { ...M3_TRUSTED_CHAIN, status: "blocked", sourceKind: "BLOCKED", detail: "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 is not DEV-LIVE proven." }, records: { operations: [ { id: "op_m3_waiting_dev_live", status: "blocked", sourceKind: "BLOCKED", summary: "Readonly placeholder; no hardware operation is issued from this panel." } ], audit: [ { id: "audit_m3_panel_contract", status: "recorded", sourceKind: "SOURCE", summary: "Workbench panel contract requires read-only evidence before trust promotion." } ], evidence: [ { id: "evi_m3_dry_run_fixture", status: "dry-run", sourceKind: "DRY-RUN", summary: "Local smoke proves shape only; it is not DEV-LIVE trusted evidence." } ] }, controls: [ { id: "m3-do1-toggle", label: "DO1 trigger", disabled: true, reason: "not wired / readonly / waiting for DEV-LIVE M3 loop" }, { id: "patch-panel-apply", label: "Apply wiring", disabled: true, reason: "not wired / readonly / waiting for DEV-LIVE M3 loop" } ] }); export function createHardwareEvidencePanelModel(data = {}) { const statuses = normalizeStatuses(data.statuses ?? defaultHardwareEvidencePanelData.statuses); const records = normalizeRecords(data.records ?? defaultHardwareEvidencePanelData.records); const controls = normalizeControls(data.controls ?? defaultHardwareEvidencePanelData.controls); const chain = normalizeChain(data.chain ?? defaultHardwareEvidencePanelData.chain); const runtimeDurability = normalizeRuntimeDurability(data.runtime ?? data.runtimeSummary); const hasTrustedDevLiveEvidence = records.evidence.some( (record) => record.sourceKind === "DEV-LIVE" && ["pass", "passed", "verified", "succeeded", "trusted"].includes(record.status) ); const chainMatches = isRequiredM3Chain(chain); const trustStatus = runtimeDurability.ready && hasTrustedDevLiveEvidence && chainMatches ? "verified" : "blocked"; const trustTone = trustStatus === "verified" ? "trusted" : "degraded"; const sourceKind = trustStatus === "verified" ? "DEV-LIVE" : "BLOCKED"; const blockedReason = runtimeDurability.ready ? "Current data lacks real DEV-LIVE evidence; the workbench must render BLOCKED/degraded, never green." : runtimeDurability.reason; const guardedChain = { ...chain, status: trustStatus === "verified" ? normalizeStatus(chain.status, "verified") : "blocked", sourceKind, detail: trustStatus === "verified" ? chain.detail : `BLOCKED: ${blockedReason}` }; return { title: data.title ?? defaultHardwareEvidencePanelData.title, subtitle: data.subtitle ?? defaultHardwareEvidencePanelData.subtitle, observedAt: data.observedAt ?? defaultHardwareEvidencePanelData.observedAt, sourceKind, sourceLegend: hardwareEvidencePanelSourceLegend, trustedLoop: { milestone: "M3", status: trustStatus, tone: trustTone, sourceKind, runtimeDurability, reason: trustStatus === "verified" ? "DEV-LIVE evidence is attached to the required M3 chain." : blockedReason }, statuses: statuses.map((status) => ({ ...status, status: trustStatus === "verified" || status.status === "blocked" ? status.status : "degraded" })), chain: guardedChain, records, controls: controls.map((control) => ({ ...control, disabled: true, reason: control.reason || "not wired / readonly / waiting for DEV-LIVE M3 loop" })) }; } export function renderHardwareEvidencePanel(container, data = {}) { if (!container || typeof container.replaceChildren !== "function") { throw new TypeError("renderHardwareEvidencePanel requires a DOM container with replaceChildren()."); } const model = createHardwareEvidencePanelModel(data); const doc = container.ownerDocument ?? globalThis.document; const root = element(doc, "aside", "hwlab-workbench-hardware-panel"); root.dataset.status = model.trustedLoop.status; root.dataset.sourceKind = model.sourceKind; root.setAttribute("aria-label", "Workbench hardware evidence panel"); root.append(panelStyles(doc)); root.append(renderHeader(doc, model)); root.append(renderStatusSection(doc, model)); root.append(renderChainSection(doc, model.chain)); root.append(renderRecordsSection(doc, model.records)); root.append(renderControlsSection(doc, model.controls)); root.append(renderLegendSection(doc, model.sourceLegend)); container.replaceChildren(root); return { element: root, model }; } function normalizeStatuses(statuses) { return arrayFrom(statuses).map((status) => ({ id: stringOr(status.id, "unknown-resource"), label: stringOr(status.label, status.id ?? "Unknown resource"), kind: stringOr(status.kind, "resource"), role: stringOr(status.role, "status"), status: normalizeStatus(status.status, "degraded"), sourceKind: normalizeSourceKind(status.sourceKind), detail: stringOr(status.detail, "No detail provided.") })); } function normalizeRecords(records) { return { operations: normalizeRecordList(records.operations), audit: normalizeRecordList(records.audit), evidence: normalizeRecordList(records.evidence) }; } function normalizeRuntimeDurability(runtime) { if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) { return { ready: false, adapter: "memory", durable: false, liveRuntimeEvidence: false, reason: "runtime.durable=false; memory/non-durable runtime cannot render DEV-LIVE or durable evidence." }; } const ready = runtime.durable === true && runtime.ready !== false && runtime.liveRuntimeEvidence === true && !["blocked", "degraded", "failed"].includes(String(runtime.status ?? "").toLowerCase()); return { ready, adapter: stringOr(runtime.adapter, "unknown"), durable: runtime.durable === true, durableRequested: runtime.durableRequested === true, liveRuntimeEvidence: runtime.liveRuntimeEvidence === true, reason: ready ? "Durable runtime readiness is proven." : `runtime durable adapter is not ready (adapter=${stringOr(runtime.adapter, "unknown")}, durable=${runtime.durable === true}); trusted records stay BLOCKED.` }; } function normalizeRecordList(records) { return arrayFrom(records).map((record) => ({ id: stringOr(record.id ?? record.operationId ?? record.auditId ?? record.evidenceId, "record"), status: normalizeStatus(record.status ?? record.outcome, "recorded"), sourceKind: normalizeSourceKind(record.sourceKind), summary: stringOr(record.summary ?? record.uri ?? record.kind, "No summary provided.") })); } function normalizeControls(controls) { return arrayFrom(controls).map((control) => ({ id: stringOr(control.id, "readonly-control"), label: stringOr(control.label, "Readonly control"), disabled: true, reason: stringOr(control.reason, "not wired / readonly / waiting for DEV-LIVE M3 loop") })); } function normalizeChain(chain) { return { fromResourceId: stringOr(chain.fromResourceId, M3_TRUSTED_CHAIN.fromResourceId), fromPort: stringOr(chain.fromPort, M3_TRUSTED_CHAIN.fromPort), patchPanelServiceId: stringOr(chain.patchPanelServiceId, M3_TRUSTED_CHAIN.patchPanelServiceId), toResourceId: stringOr(chain.toResourceId, M3_TRUSTED_CHAIN.toResourceId), toPort: stringOr(chain.toPort, M3_TRUSTED_CHAIN.toPort), status: normalizeStatus(chain.status, "blocked"), sourceKind: normalizeSourceKind(chain.sourceKind), detail: stringOr(chain.detail, "Waiting for M3 evidence.") }; } function isRequiredM3Chain(chain) { return ( chain.fromResourceId === M3_TRUSTED_CHAIN.fromResourceId && chain.fromPort === M3_TRUSTED_CHAIN.fromPort && chain.patchPanelServiceId === M3_TRUSTED_CHAIN.patchPanelServiceId && chain.toResourceId === M3_TRUSTED_CHAIN.toResourceId && chain.toPort === M3_TRUSTED_CHAIN.toPort ); } function normalizeStatus(status, fallback) { const value = String(status ?? fallback).trim().toLowerCase(); return value || fallback; } function normalizeSourceKind(sourceKind) { const value = String(sourceKind ?? "SOURCE").trim().toUpperCase(); return SOURCE_KIND_ORDER.includes(value) ? value : "SOURCE"; } function arrayFrom(value) { return Array.isArray(value) ? value : []; } function stringOr(value, fallback) { const text = value === undefined || value === null ? "" : String(value).trim(); return text || fallback; } function renderHeader(doc, model) { const header = element(doc, "header", "hwlab-panel-header"); const titleWrap = element(doc, "div", "hwlab-panel-title-wrap"); titleWrap.append(textElement(doc, "p", "hwlab-panel-kicker", "M3 trusted loop")); titleWrap.append(textElement(doc, "h2", "hwlab-panel-title", model.title)); titleWrap.append(textElement(doc, "p", "hwlab-panel-subtitle", model.subtitle)); header.append(titleWrap); header.append(badge(doc, model.sourceKind, model.trustedLoop.tone)); header.append(textElement(doc, "p", `hwlab-panel-trust tone-${toneClass(model.trustedLoop.tone)}`, model.trustedLoop.reason)); return header; } function renderStatusSection(doc, model) { const section = panelSection(doc, "Hardware status", "BOX-SIMU / Gateway-SIMU / Patch Panel"); const grid = element(doc, "div", "hwlab-status-grid"); for (const item of model.statuses) { const card = element(doc, "article", `hwlab-status-card tone-${toneClass(item.status)}`); card.dataset.sourceKind = item.sourceKind; const head = element(doc, "div", "hwlab-status-card-head"); head.append(textElement(doc, "strong", "hwlab-status-label", item.label)); head.append(badge(doc, item.sourceKind, item.sourceKind)); card.append(head); card.append(textElement(doc, "span", "hwlab-status-kind", `${item.kind} / ${item.role}`)); card.append(textElement(doc, "span", `hwlab-status-value tone-${toneClass(item.status)}`, item.status.toUpperCase())); card.append(textElement(doc, "p", "hwlab-status-detail", item.detail)); grid.append(card); } section.append(grid); return section; } function renderChainSection(doc, chain) { const section = panelSection(doc, "M3 chain", "Required route"); const chainLine = element(doc, "div", `hwlab-chain-line tone-${toneClass(chain.status)}`); chainLine.append(chainNode(doc, `${chain.fromResourceId}:${chain.fromPort}`, "BOX-SIMU DO1")); chainLine.append(textElement(doc, "span", "hwlab-chain-arrow", "->")); chainLine.append(chainNode(doc, chain.patchPanelServiceId, "Patch Panel")); chainLine.append(textElement(doc, "span", "hwlab-chain-arrow", "->")); chainLine.append(chainNode(doc, `${chain.toResourceId}:${chain.toPort}`, "BOX-SIMU DI1")); section.append(chainLine); const detail = element(doc, "div", "hwlab-chain-detail"); detail.append(badge(doc, chain.sourceKind, chain.status)); detail.append(textElement(doc, "p", "hwlab-chain-copy", chain.detail)); section.append(detail); return section; } function renderRecordsSection(doc, records) { const section = panelSection(doc, "Records", "operation / audit / evidence summary"); const columns = element(doc, "div", "hwlab-record-columns"); for (const [label, items] of [ ["operation", records.operations], ["audit", records.audit], ["evidence", records.evidence] ]) { const column = element(doc, "article", "hwlab-record-column"); column.append(textElement(doc, "h4", "hwlab-record-heading", `${label} (${items.length})`)); const list = element(doc, "ul", "hwlab-record-list"); for (const item of items) { const row = element(doc, "li", "hwlab-record-item"); const head = element(doc, "div", "hwlab-record-head"); head.append(textElement(doc, "span", "hwlab-record-id", item.id)); head.append(badge(doc, item.sourceKind, item.sourceKind)); row.append(head); row.append(textElement(doc, "span", `hwlab-record-status tone-${toneClass(item.status)}`, item.status)); row.append(textElement(doc, "p", "hwlab-record-summary", item.summary)); list.append(row); } column.append(list); columns.append(column); } section.append(columns); return section; } function renderControlsSection(doc, controls) { const section = panelSection(doc, "Controls", "readonly placeholders"); const list = element(doc, "div", "hwlab-control-list"); for (const control of controls) { const row = element(doc, "div", "hwlab-control-row"); const button = element(doc, "button", "hwlab-control-button"); button.type = "button"; button.disabled = true; button.dataset.controlId = control.id; button.textContent = control.label; row.append(button); row.append(textElement(doc, "span", "hwlab-control-reason", control.reason)); list.append(row); } section.append(list); return section; } function renderLegendSection(doc, legend) { const section = panelSection(doc, "Source labels", "promotion boundary"); const list = element(doc, "dl", "hwlab-source-legend"); for (const item of legend) { list.append(textElement(doc, "dt", "hwlab-source-kind", item.label)); list.append(textElement(doc, "dd", "hwlab-source-detail", item.detail)); } section.append(list); return section; } function panelSection(doc, title, caption) { const section = element(doc, "section", "hwlab-panel-section"); const head = element(doc, "div", "hwlab-section-head"); head.append(textElement(doc, "h3", "hwlab-section-title", title)); head.append(textElement(doc, "span", "hwlab-section-caption", caption)); section.append(head); return section; } function chainNode(doc, value, label) { const node = element(doc, "span", "hwlab-chain-node"); node.append(textElement(doc, "strong", "hwlab-chain-value", value)); node.append(textElement(doc, "small", "hwlab-chain-label", label)); return node; } function badge(doc, value, tone) { const item = element(doc, "span", `hwlab-source-badge tone-${toneClass(tone ?? value)}`); item.textContent = value; return item; } function textElement(doc, tagName, className, text) { const item = element(doc, tagName, className); item.textContent = text; return item; } function element(doc, tagName, className) { const item = doc.createElement(tagName); if (className) item.className = className; return item; } function toneClass(value) { return String(value ?? "unknown") .trim() .replace(/[^a-z0-9_-]/giu, "-") .toLowerCase(); } function panelStyles(doc) { const style = element(doc, "style"); style.textContent = ` .hwlab-workbench-hardware-panel { box-sizing: border-box; display: grid; gap: 12px; min-width: 320px; max-width: 420px; padding: 14px; color: #d9e2df; background: #151a1d; border-left: 1px solid #364246; font: 13px/1.45 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } .hwlab-workbench-hardware-panel *, .hwlab-workbench-hardware-panel *::before, .hwlab-workbench-hardware-panel *::after { box-sizing: border-box; } .hwlab-panel-header, .hwlab-panel-section { display: grid; gap: 10px; padding: 12px; background: #1d2427; border: 1px solid #364246; border-radius: 6px; } .hwlab-panel-title-wrap { display: grid; gap: 2px; } .hwlab-section-head, .hwlab-status-card-head, .hwlab-record-head, .hwlab-control-row, .hwlab-chain-detail { display: flex; gap: 8px; align-items: center; justify-content: space-between; } .hwlab-panel-title, .hwlab-section-title, .hwlab-record-heading, .hwlab-panel-kicker, .hwlab-panel-subtitle, .hwlab-panel-trust, .hwlab-status-detail, .hwlab-chain-copy, .hwlab-record-summary { margin: 0; } .hwlab-panel-title { font-size: 18px; font-weight: 700; } .hwlab-panel-kicker, .hwlab-section-caption, .hwlab-status-kind, .hwlab-chain-label, .hwlab-panel-subtitle, .hwlab-source-detail, .hwlab-control-reason { color: #95a6a3; } .hwlab-source-badge { flex: 0 0 auto; padding: 2px 7px; border: 1px solid currentColor; border-radius: 999px; font-size: 11px; font-weight: 700; letter-spacing: 0; } .hwlab-status-grid, .hwlab-record-columns { display: grid; gap: 8px; } .hwlab-status-card, .hwlab-record-column, .hwlab-record-item, .hwlab-chain-line { display: grid; gap: 6px; padding: 10px; background: #101416; border: 1px solid #2c373a; border-radius: 6px; } .hwlab-status-value, .hwlab-record-status, .hwlab-record-id, .hwlab-chain-value { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } .hwlab-chain-line { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; } .hwlab-chain-node { display: grid; gap: 2px; min-width: 0; } .hwlab-chain-value { overflow-wrap: anywhere; } .hwlab-chain-arrow { color: #95a6a3; } .hwlab-record-list, .hwlab-source-legend { display: grid; gap: 8px; margin: 0; padding: 0; } .hwlab-record-list { list-style: none; } .hwlab-source-legend { grid-template-columns: auto minmax(0, 1fr); } .hwlab-source-kind { margin: 0; font-weight: 700; } .hwlab-source-detail { margin: 0; } .hwlab-control-list { display: grid; gap: 8px; } .hwlab-control-button { min-width: 104px; padding: 6px 9px; color: #95a6a3; background: #222a2d; border: 1px solid #3a474b; border-radius: 4px; cursor: not-allowed; } .tone-blocked, .tone-degraded { color: #ffb86b; } .tone-source, .tone-dry-run { color: #8fc7ff; } .tone-trusted, .tone-dev-live, .tone-verified { color: #74d99f; } `; return style; }