590 lines
19 KiB
JavaScript
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;
|
|
}
|