Files
pikasTech-HWLAB/web/hwlab-cloud-web/workbench-hardware-panel.mjs
T
2026-05-22 23:38:18 +00:00

590 lines
19 KiB
JavaScript

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;
}