From 166a7763a4d2fd7be9a801caf0a51b4be3e59137 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Fri, 22 May 2026 17:36:27 +0800 Subject: [PATCH] feat: add HWLAB Cloud workbench shell Refs #99 #78 --- web/hwlab-cloud-web/app.mjs | 952 +++++++++++++--------- web/hwlab-cloud-web/index.html | 438 +++++----- web/hwlab-cloud-web/scripts/check.mjs | 46 +- web/hwlab-cloud-web/styles.css | 1072 +++++++++++++++++-------- 4 files changed, 1522 insertions(+), 986 deletions(-) diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index fa368a65..e5e9d00b 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -9,42 +9,50 @@ const rpcReadMethods = Object.freeze([ "evidence.record.query" ]); -const navLinks = [...document.querySelectorAll("[data-route]")]; -const views = [...document.querySelectorAll("[data-view]")]; -const viewIds = new Set(views.map((view) => view.dataset.view)); +const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view)); let rpcSequence = 0; const el = { + shell: query("[data-app-shell]"), + explorerToggle: byId("explorer-toggle"), + explorerStatus: byId("explorer-status"), + sourceStrip: byId("source-strip"), + treeCount: byId("tree-count"), + resourceTree: byId("resource-tree"), + blockerCount: byId("blocker-count"), + explorerBlockers: byId("explorer-blockers"), + routePath: byId("route-path"), liveStatus: byId("live-status"), liveDetail: byId("live-detail"), - overviewMetrics: byId("overview-metrics"), - methodDetail: byId("method-detail"), - methodGrid: byId("method-grid"), - runtimeDetail: byId("runtime-detail"), - runtimeMetrics: byId("runtime-metrics"), - topologyLiveMetrics: byId("topology-live-metrics"), - topologyBody: byId("topology-body"), - patchMetrics: byId("patch-metrics"), - hardwareLiveMetrics: byId("hardware-live-metrics"), - operationBody: byId("operation-body"), - agentMetrics: byId("agent-metrics"), - agentEvidenceMetrics: byId("agent-evidence-metrics"), - auditDetail: byId("audit-detail"), - auditBody: byId("audit-body"), - evidenceQueryDetail: byId("evidence-query-detail"), - liveEvidenceBody: byId("live-evidence-body"), - evidenceBody: byId("evidence-body"), - metaGrid: byId("meta-grid"), + conversationList: byId("conversation-list"), + traceList: byId("trace-list"), + gateMetadata: byId("gate-metadata"), milestoneGrid: byId("milestone-grid"), healthBody: byId("health-body"), - blockerList: byId("blocker-list"), - commandList: byId("command-list") + gateBlockers: byId("gate-blockers"), + validationList: byId("validation-list"), + commandForm: byId("command-form"), + commandInput: byId("command-input"), + commandClear: byId("command-clear"), + hardwareList: byId("hardware-list"), + controlList: byId("control-list"), + wiringList: byId("wiring-list"), + recordsList: byId("records-list"), + diagnosticsList: byId("diagnostics-list"), + methodList: byId("method-list") +}; + +const state = { + drafts: [] }; initRoutes(); -renderStaticConsole(); -renderLivePending(); -loadLiveSurface().then(renderLiveConsole); +initExplorerToggle(); +initSideTabs(); +initCommandBar(); +renderStaticWorkbench(); +renderProbePending(); +loadLiveSurface().then(renderLiveSurface); function byId(id) { const element = document.getElementById(id); @@ -54,8 +62,21 @@ function byId(id) { return element; } +function query(selector) { + const element = document.querySelector(selector); + if (!element) { + throw new Error(`missing required element ${selector}`); + } + return element; +} + function initRoutes() { window.addEventListener("hashchange", () => showView(routeFromLocation())); + for (const button of document.querySelectorAll("[data-route]")) { + button.addEventListener("click", () => { + window.location.hash = button.dataset.route; + }); + } showView(routeFromLocation()); } @@ -63,64 +84,99 @@ function routeFromLocation() { const hashRoute = window.location.hash.replace(/^#/, ""); if (viewIds.has(hashRoute)) return hashRoute; if (window.location.pathname.replace(/\/+$/, "").endsWith("/gate")) return "gate"; - return "overview"; + return "workspace"; } function showView(route) { - const activeRoute = viewIds.has(route) ? route : "overview"; - for (const view of views) { + const activeRoute = viewIds.has(route) ? route : "workspace"; + for (const view of document.querySelectorAll("[data-view]")) { view.hidden = view.dataset.view !== activeRoute; } - for (const link of navLinks) { - link.classList.toggle("active", link.dataset.route === activeRoute); - link.setAttribute("aria-current", link.dataset.route === activeRoute ? "page" : "false"); + for (const button of document.querySelectorAll("[data-route]")) { + const active = button.dataset.route === activeRoute; + button.classList.toggle("active", active); + button.setAttribute("aria-current", active ? "page" : "false"); } } -function renderStaticConsole() { - renderTopologyContract(); - renderHardwareContract(); - renderAgentContract(); - renderEvidenceContract(); +function initExplorerToggle() { + el.explorerToggle.addEventListener("click", () => { + const collapsed = !el.shell.classList.contains("explorer-collapsed"); + el.shell.classList.toggle("explorer-collapsed", collapsed); + el.explorerToggle.setAttribute("aria-pressed", collapsed ? "true" : "false"); + }); +} + +function initSideTabs() { + for (const tab of document.querySelectorAll("[data-side-tab]")) { + tab.addEventListener("click", () => selectSideTab(tab.dataset.sideTab)); + } + for (const jump of document.querySelectorAll("[data-side-tab-jump]")) { + jump.addEventListener("click", () => selectSideTab(jump.dataset.sideTabJump)); + } +} + +function selectSideTab(tabId) { + for (const tab of document.querySelectorAll("[data-side-tab]")) { + const active = tab.dataset.sideTab === tabId; + tab.classList.toggle("active", active); + tab.setAttribute("aria-selected", active ? "true" : "false"); + } + for (const panel of document.querySelectorAll("[data-side-panel]")) { + const active = panel.dataset.sidePanel === tabId; + panel.classList.toggle("active", active); + panel.hidden = !active; + } +} + +function initCommandBar() { + el.commandForm.addEventListener("submit", (event) => { + event.preventDefault(); + const value = el.commandInput.value.trim(); + if (!value) return; + state.drafts.unshift({ + text: value, + createdAt: new Date().toISOString() + }); + el.commandInput.value = ""; + renderDrafts(); + }); + + el.commandClear.addEventListener("click", () => { + state.drafts = []; + renderDrafts(); + }); +} + +function renderStaticWorkbench() { + el.explorerStatus.textContent = gateSummary.blocked ? "blocked" : gateSummary.gateStatus; + el.explorerStatus.className = `status-dot tone-${toneClass(gateSummary.blocked ? "blocked" : gateSummary.gateStatus)}`; + el.routePath.textContent = runtime.serviceRoute.join(" -> "); + + renderStateStrip(false); + renderResourceTree(); + renderBlockerLists(); + renderConversation(); + renderTrace(); + renderHardwareStatus(null); + renderDrafts(); + renderWiringList(); + renderRecords(null); + renderDiagnostics(null); renderGateDiagnostics(); } -function renderLivePending() { - appendMetricGrid(el.overviewMetrics, [ - ["API origin", "same-origin /v1 + /json-rpc"], - ["Frontend", runtime.endpoints.frontend, "warn"], - ["Configured DEV", runtime.endpoints.dev, "warn"], - ["Health", "probing", "warn"], - ["Runtime", "probing", "warn"], - ["M3", "probing", "warn"], - ["Gate", gateSummary.gateStatus, gateSummary.gateStatus] - ]); - replaceChildren(el.methodGrid, emptyNotice("Waiting for live cloud-api method list.")); - appendMetricGrid(el.runtimeMetrics, [ - ["Store", "probing", "warn"], - ["Durable", "probing", "warn"], - ["Gateway sessions", "probing", "warn"], - ["Evidence records", "probing", "warn"] - ]); - appendMetricGrid(el.topologyLiveMetrics, [ - ["Live gateway sessions", "probing", "warn"], - ["Live box resources", "probing", "warn"], - ["Live capabilities", "probing", "warn"], - ["Live patch state", "not exposed read-only", "warn"] - ]); - appendMetricGrid(el.hardwareLiveMetrics, [ - ["Live operations", "probing", "warn"], - ["Live audit events", "probing", "warn"], - ["Live evidence records", "probing", "warn"], - ["Mutation surface", "not called by console", "pass"] - ]); - rowNotice(el.auditBody, 4, "Waiting for audit.event.query."); - rowNotice(el.liveEvidenceBody, 4, "Waiting for evidence.record.query."); +function renderProbePending() { + el.liveStatus.textContent = "probing"; + el.liveStatus.className = "tone-probing"; + el.liveDetail.textContent = "Checking /health/live, /v1, and read-only /json-rpc from this origin."; + renderMethodList(rpcReadMethods, "pending"); } async function loadLiveSurface() { const projectId = gateSummary.topology.projectId; - const [restIndex, health, adapter, audit, evidence] = await Promise.all([ + const [healthLive, restIndex, health, adapter, audit, evidence] = await Promise.all([ + fetchJson("/health/live"), fetchJson("/v1"), callRpc("system.health"), callRpc("cloud.adapter.describe"), @@ -130,6 +186,7 @@ async function loadLiveSurface() { return { observedAt: new Date().toISOString(), + healthLive, restIndex, health, adapter, @@ -213,305 +270,481 @@ async function callRpc(method, params = {}) { function nextProtocolId(prefix) { rpcSequence += 1; - return `${prefix}_cloud-web-console-${Date.now()}-${rpcSequence}`; + return `${prefix}_cloud-web-workbench-${Date.now()}-${rpcSequence}`; } -function renderLiveConsole(live) { - const coreProbes = [live.restIndex, live.health, live.adapter]; +function renderLiveSurface(live) { + const coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]; const reachable = coreProbes.some((probe) => probe.ok); + const healthStatus = live.health.data?.status ?? live.healthLive.data?.status ?? "degraded"; const runtimeSummary = runtimeSummaryFrom(live); - const methodList = methodsFrom(live); - const healthStatus = live.health.ok ? live.health.data.status : "degraded"; - const liveTone = reachable ? healthStatus : "degraded"; + const methods = methodsFrom(live); + const dbReady = dbReadyFrom(live); + const devLiveReady = reachable && healthStatus === "ok" && dbReady === true && !gateSummary.blocked; + const blocked = gateSummary.blocked || !devLiveReady; - el.liveStatus.textContent = reachable ? healthStatus : "degraded"; - el.liveStatus.className = `tone-${toneClass(liveTone)}`; + el.liveStatus.textContent = devLiveReady ? "dev-live ready" : reachable ? "blocked" : "degraded"; + el.liveStatus.className = `tone-${toneClass(devLiveReady ? "dev-live" : reachable ? "blocked" : "degraded")}`; el.liveDetail.textContent = reachable - ? `Same-origin cloud-api responded; runtime status is ${healthStatus}.` + ? `Same-origin read surface responded; health=${healthStatus}, db.ready=${String(dbReady)}, DEV-LIVE=${devLiveReady ? "true" : "false"}.` : `Same-origin API unavailable: ${probeErrors(coreProbes).join(" | ")}`; - appendMetricGrid(el.overviewMetrics, [ - ["API origin", "same-origin /v1 + /json-rpc"], - ["Frontend", runtime.endpoints.frontend, "warn"], - ["Configured DEV", runtime.endpoints.dev, "warn"], - ["Health", live.health.ok ? live.health.data.status : "degraded", live.health.ok ? live.health.data.status : "degraded"], - ["Runtime", runtimeSummary?.status ?? "unavailable", runtimeSummary?.status ?? "degraded"], - ["M3", m3Status(), m3Status()], - ["Observed", reachable ? shortTime(live.observedAt) : "not available", reachable ? "pass" : "degraded"] - ]); + renderStateStrip(devLiveReady); + renderHardwareStatus(runtimeSummary); + renderRecords(live); + renderDiagnostics(live); + renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, reachable ? "live" : "degraded"); - renderMethods(methodList, live); - renderRuntimeSummary(runtimeSummary, live); - renderLiveTopology(runtimeSummary); - renderLiveHardware(runtimeSummary); - renderLiveAudit(live.audit); - renderLiveEvidence(live.evidence); -} - -function renderMethods(methods, live) { - if (methods.length === 0) { - el.methodDetail.textContent = `degraded: no live method list returned. ${probeErrors([live.restIndex, live.adapter]).join(" | ")}`; - replaceChildren(el.methodGrid, emptyNotice("No live cloud-api methods available.")); - return; - } - - const source = live.restIndex.ok && Array.isArray(live.restIndex.data.methods) ? "GET /v1" : "cloud.adapter.describe"; - el.methodDetail.textContent = `Live method list from ${source}; showing read-only diagnostics methods from ${methods.length} returned methods.`; - const fragment = document.createDocumentFragment(); - for (const method of methods) { - if (!rpcReadMethods.includes(method)) continue; - const item = document.createElement("span"); - item.className = "method-pill read"; - item.textContent = method; - fragment.append(item); - } - replaceChildren(el.methodGrid, fragment); -} - -function renderRuntimeSummary(summary, live) { - if (!summary) { - el.runtimeDetail.textContent = `degraded: runtime summary unavailable. ${probeErrors([live.restIndex, live.health, live.adapter]).join(" | ")}`; - appendMetricGrid(el.runtimeMetrics, [ - ["Store", "unavailable", "degraded"], - ["Durable", "unavailable", "degraded"], - ["Gateway sessions", "unavailable", "degraded"], - ["Evidence records", "unavailable", "degraded"] - ]); - return; - } - - el.runtimeDetail.textContent = summary.reason || "Live cloud-api returned a runtime summary."; - const counts = summary.counts ?? {}; - appendMetricGrid(el.runtimeMetrics, [ - ["Store", summary.adapter ?? "unknown", summary.status ?? "degraded"], - ["Durable", summary.durable === true ? "true" : "false", summary.durable === true ? "pass" : "warn"], - ["Gateway sessions", counts.gatewaySessions ?? 0, "pass"], - ["Evidence records", counts.evidenceRecords ?? 0, "pass"] - ]); -} - -function renderLiveTopology(summary) { - const counts = summary?.counts; - appendMetricGrid(el.topologyLiveMetrics, [ - ["Live gateway sessions", valueOrUnavailable(counts?.gatewaySessions), summary ? "pass" : "degraded"], - ["Live box resources", valueOrUnavailable(counts?.boxResources), summary ? "pass" : "degraded"], - ["Live capabilities", valueOrUnavailable(counts?.boxCapabilities), summary ? "pass" : "degraded"], - ["Live patch state", "not exposed read-only", "warn"] - ]); -} - -function renderLiveHardware(summary) { - const counts = summary?.counts; - appendMetricGrid(el.hardwareLiveMetrics, [ - ["Live operations", valueOrUnavailable(counts?.hardwareOperations), summary ? "pass" : "degraded"], - ["Live audit events", valueOrUnavailable(counts?.auditEvents), summary ? "pass" : "degraded"], - ["Live evidence records", valueOrUnavailable(counts?.evidenceRecords), summary ? "pass" : "degraded"], - ["Mutation surface", "not called by console", "pass"] - ]); -} - -function renderLiveAudit(auditProbe) { - el.auditBody.replaceChildren(); - if (!auditProbe.ok) { - el.auditDetail.textContent = `degraded: ${auditProbe.error}`; - rowNotice(el.auditBody, 4, "No live audit records returned.", "degraded"); - return; - } - - const events = auditProbe.data?.events ?? []; - el.auditDetail.textContent = `Live audit.event.query returned ${auditProbe.data?.count ?? events.length} records.`; - if (events.length === 0) { - rowNotice(el.auditBody, 4, "cloud-api returned zero live audit records.", "muted"); - return; - } - for (const event of events) { - const tr = document.createElement("tr"); - tr.append(cell(event.auditId, "mono")); - tr.append(cell(event.action)); - tr.append(cell(`${event.targetType ?? "target"}:${event.targetId ?? "n/a"}`, "mono wrap")); - const statusCell = document.createElement("td"); - statusCell.append(badge(event.outcome ?? "recorded", event.outcome ?? "pass")); - tr.append(statusCell); - el.auditBody.append(tr); + if (blocked) { + el.explorerStatus.textContent = "blocked"; + el.explorerStatus.className = "status-dot tone-blocked"; } } -function renderLiveEvidence(evidenceProbe) { - el.liveEvidenceBody.replaceChildren(); - if (!evidenceProbe.ok) { - el.evidenceQueryDetail.textContent = `degraded: ${evidenceProbe.error}`; - rowNotice(el.liveEvidenceBody, 4, "No live evidence records returned.", "degraded"); - return; - } - - const records = evidenceProbe.data?.records ?? []; - el.evidenceQueryDetail.textContent = `Live evidence.record.query returned ${evidenceProbe.data?.count ?? records.length} records.`; - if (records.length === 0) { - rowNotice(el.liveEvidenceBody, 4, "cloud-api returned zero live evidence records.", "muted"); - return; - } - for (const record of records) { - const tr = document.createElement("tr"); - tr.append(cell(record.evidenceId, "mono")); - tr.append(cell(record.operationId, "mono")); - tr.append(cell(record.kind)); - tr.append(cell(record.serviceId, "mono")); - el.liveEvidenceBody.append(tr); - } +function renderStateStrip(devLiveReady) { + replaceChildren( + el.sourceStrip, + stateTag("SOURCE", `report ${shortHash(gateSummary.reportCommitId)}`), + stateTag("DRY-RUN", gateSummary.dryRun.status), + stateTag("DEV-LIVE", devLiveReady ? "ready" : "not claimed"), + stateTag("BLOCKED", gateSummary.blocked ? `${gateSummary.blockers.length} open` : "clear") + ); } -function renderTopologyContract() { - el.topologyBody.replaceChildren(); - for (const gateway of gateSummary.topology.gateways) { - const tr = document.createElement("tr"); - tr.append(cell("Gateway")); - tr.append(cell(gateway.gatewaySessionId, "mono")); - tr.append(cell(gateway.serviceId, "mono")); - const statusCell = document.createElement("td"); - statusCell.append(badge(gateway.status, gateway.status)); - tr.append(statusCell); - tr.append(cell(gateway.endpoint ?? gateway.gatewayId, "mono wrap")); - el.topologyBody.append(tr); - } - for (const resource of gateSummary.topology.boxResources) { - const tr = document.createElement("tr"); - tr.append(cell("Box resource")); - tr.append(cell(resource.resourceId, "mono")); - tr.append(cell(resource.gatewaySessionId, "mono")); - const statusCell = document.createElement("td"); - statusCell.append(badge(resource.state, resource.state)); - tr.append(statusCell); - tr.append(cell(`${resource.resourceType}: ${resource.name ?? resource.boxId}`, "wrap")); - el.topologyBody.append(tr); - } +function renderResourceTree() { + const items = [ + { + label: gateSummary.topology.projectId, + meta: `${gateSummary.environment} project`, + tone: "source", + children: [ + ...gateSummary.topology.gateways.map((gateway) => ({ + label: gateway.gatewaySessionId, + meta: `${gateway.serviceId} / ${gateway.status}`, + tone: gateway.status + })), + ...gateSummary.topology.boxResources.map((resource) => ({ + label: resource.resourceId, + meta: `${resource.resourceType} / ${resource.state}`, + tone: resource.state + })), + { + label: gateSummary.topology.patchPanel.patchPanelStatusId, + meta: `${gateSummary.topology.patchPanel.serviceId} / ${gateSummary.topology.patchPanel.state}`, + tone: gateSummary.topology.patchPanel.state + } + ] + }, + { + label: gateSummary.agent.agentSessionId, + meta: `${gateSummary.agent.agentServiceId} / ${gateSummary.agent.agentStatus}`, + tone: gateSummary.agent.agentStatus, + children: [ + { + label: gateSummary.agent.workerSessionId, + meta: `${gateSummary.agent.workerServiceId} / ${gateSummary.agent.workerStatus}`, + tone: gateSummary.agent.workerStatus + } + ] + }, + { + label: "diagnostics", + meta: "secondary Gate view", + tone: "source", + children: gateSummary.milestones.map((milestone) => ({ + label: milestone.id, + meta: milestone.status, + tone: milestone.status + })) + } + ]; - appendMetricGrid(el.patchMetrics, [ - ["Patch panel", gateSummary.topology.patchPanel.patchPanelStatusId], - ["Service", gateSummary.topology.patchPanel.serviceId], - ["State", gateSummary.topology.patchPanel.state, gateSummary.topology.patchPanel.state], - [ - "Active links", - gateSummary.topology.patchPanel.activeConnections - .map((link) => `${link.fromResourceId}:${link.fromPort} -> ${link.toResourceId}:${link.toPort}`) - .join(", ") - ] - ]); + el.treeCount.textContent = String(items.reduce((count, item) => count + 1 + (item.children?.length ?? 0), 0)); + replaceChildren(el.resourceTree, ...items.map(treeGroup)); } -function renderHardwareContract() { - el.operationBody.replaceChildren(); - for (const operation of gateSummary.operations) { - const tr = document.createElement("tr"); - tr.append(cell(operation.operationId, "mono")); - tr.append(cell(operation.requestedBy, "mono")); - const statusCell = document.createElement("td"); - statusCell.append(badge(operation.status, operation.status)); - tr.append(statusCell); - tr.append(cell(`${operation.resourceId} / ${operation.capabilityId}`, "mono wrap")); - tr.append(cell([operation.agentSessionId, operation.workerSessionId].filter(Boolean).join(" / ") || "direct", "mono")); - el.operationBody.append(tr); +function treeGroup(item) { + const details = document.createElement("details"); + details.open = true; + const summary = document.createElement("summary"); + summary.append(statusLight(item.tone), textSpan(item.label, "tree-label"), textSpan(item.meta, "tree-meta")); + details.append(summary); + for (const child of item.children ?? []) { + const row = document.createElement("button"); + row.type = "button"; + row.className = "tree-row"; + row.append(statusLight(child.tone), textSpan(child.label, "tree-label"), textSpan(child.meta, "tree-meta")); + details.append(row); } + return details; } -function renderAgentContract() { - appendMetricGrid(el.agentMetrics, [ - ["Agent session", gateSummary.agent.agentSessionId], - ["Agent service", gateSummary.agent.agentServiceId], - ["Worker session", gateSummary.agent.workerSessionId], - ["Worker service", gateSummary.agent.workerServiceId], - ["Agent status", gateSummary.agent.agentStatus, gateSummary.agent.agentStatus], - ["Worker status", gateSummary.agent.workerStatus, gateSummary.agent.workerStatus] - ]); - appendMetricGrid(el.agentEvidenceMetrics, [ - ["Trace events", gateSummary.traceCount], - ["Audit events", gateSummary.auditCount], - ["Evidence records", gateSummary.evidenceCount], - ["Cleanup records", gateSummary.cleanupCount], - ["Cleanup id", gateSummary.agent.cleanupId], - ["Source", "checked-in gate report", "warn"] - ]); +function renderBlockerLists() { + el.blockerCount.textContent = String(gateSummary.blockers.length); + replaceChildren(el.explorerBlockers, ...gateSummary.blockers.map(blockerCard)); + replaceChildren(el.gateBlockers, ...gateSummary.blockers.map(blockerCard)); } -function renderEvidenceContract() { - el.evidenceBody.replaceChildren(); - for (const record of gateSummary.evidenceRecords) { - const tr = document.createElement("tr"); - tr.append(cell(record.evidenceId, "mono")); - tr.append(cell(record.operationId, "mono")); - tr.append(cell(record.serviceId, "mono")); - tr.append(cell(record.uri, "mono wrap")); - tr.append(cell(record.sha256, "mono wrap")); - el.evidenceBody.append(tr); +function renderConversation() { + const messages = [ + { + role: "system", + title: "Surface mode", + text: "This shell is scoped to M3 trusted-loop users. It shows route, topology, trace, and diagnostics without dispatching hardware mutations." + }, + { + role: "agent", + title: "Live acceptance state", + text: gateSummary.devPreconditions.summary + }, + ...gateSummary.blockers.map((blocker) => ({ + role: "blocker", + title: blocker.scope, + text: blocker.summary + })), + { + role: "user", + title: "Available action", + text: "Draft an instruction in the bottom command bar. Drafts remain local in this browser session until a trusted backend workflow exists." + } + ]; + replaceChildren(el.conversationList, ...messages.map(messageCard)); +} + +function renderTrace() { + replaceChildren( + el.traceList, + ...gateSummary.steps.map((step) => { + const article = document.createElement("article"); + article.className = "trace-item"; + article.append(textSpan(String(step.order).padStart(2, "0"), "trace-order")); + const body = document.createElement("div"); + body.append(textSpan(step.title, "trace-title")); + body.append(textSpan(`${step.kind} / ${step.outputs.length} outputs`, "trace-meta")); + article.append(body, badge(step.requires.length === 0 ? "entry" : "requires previous", step.requires.length === 0 ? "source" : "dry-run")); + return article; + }) + ); +} + +function renderHardwareStatus(summary) { + const counts = summary?.counts ?? {}; + const cards = [ + { + title: "Gateway-SIMU", + value: valueOrUnavailable(counts.gatewaySessions, gateSummary.gatewaySessionCount), + meta: "read count; source fallback", + tone: summary ? "dev-live" : "source" + }, + { + title: "BOX-SIMU", + value: valueOrUnavailable(counts.boxResources, gateSummary.boxResourceCount), + meta: "read count; source fallback", + tone: summary ? "dev-live" : "source" + }, + { + title: "Patch Panel", + value: gateSummary.topology.patchPanel.state, + meta: `${gateSummary.topology.patchPanel.activeConnectionCount} source links`, + tone: gateSummary.blocked ? "blocked" : gateSummary.topology.patchPanel.state + }, + { + title: "Mutation Surface", + value: "disabled", + meta: "no hardware write RPC from web", + tone: "blocked" + } + ]; + replaceChildren(el.hardwareList, ...cards.map(statusCard)); +} + +function controlRows() { + return [ + { + title: "Hardware operation", + detail: "Not exposed in this web shell. Use patch-panel governed workflows after blockers clear.", + tone: "blocked" + }, + { + title: "Agent automation", + detail: "Visible as trusted trace and dry-run evidence; live scheduling remains blocked by DB readiness.", + tone: "dry-run" + }, + { + title: "Read probes", + detail: "Same-origin /health/live, /v1, and read-only /json-rpc diagnostics are allowed.", + tone: "source" + } + ]; +} + +function renderDrafts() { + const rows = [ + ...state.drafts.map((draft) => ({ + title: `Local draft ${shortTime(draft.createdAt)}`, + detail: draft.text, + tone: "dry-run" + })), + ...(state.drafts.length === 0 + ? [ + { + title: "Draft queue", + detail: "No local drafts. The input bar records text locally and does not call cloud-api mutation methods.", + tone: "source" + } + ] + : []), + ...controlRows() + ]; + replaceChildren(el.controlList, ...rows.map(infoCard)); +} + +function renderWiringList() { + const patchPanel = gateSummary.topology.patchPanel; + const cards = [ + infoCard({ + title: patchPanel.serviceId, + detail: `${patchPanel.patchPanelStatusId} / ${patchPanel.state} / ${patchPanel.environment}`, + tone: patchPanel.state + }), + ...patchPanel.activeConnections.map((link) => + infoCard({ + title: `${link.fromResourceId}:${link.fromPort}`, + detail: `patched to ${link.toResourceId}:${link.toPort}`, + tone: "source" + }) + ) + ]; + replaceChildren(el.wiringList, ...cards); +} + +function renderRecords(live) { + const sourceCards = gateSummary.evidenceRecords.map((record) => + infoCard({ + title: record.evidenceId, + detail: `${record.kind} / ${record.operationId} / ${record.dryRun ? "DRY-RUN" : "unknown source"}`, + tone: record.dryRun ? "dry-run" : "source" + }) + ); + + const liveRecords = live?.evidence.ok ? live.evidence.data?.records ?? [] : []; + const liveCards = liveRecords.map((record) => + infoCard({ + title: record.evidenceId, + detail: `${record.kind ?? "record"} / ${record.operationId ?? "n/a"} / ${record.serviceId ?? "unknown service"}`, + tone: "dev-live" + }) + ); + if (live && !live.evidence.ok) { + liveCards.push(infoCard({ title: "evidence.record.query", detail: live.evidence.error, tone: "blocked" })); } + + replaceChildren(el.recordsList, ...liveCards, ...sourceCards); +} + +function renderDiagnostics(live) { + const diagnostics = [ + { + title: "Frontend", + detail: runtime.endpoints.frontend, + tone: "source" + }, + { + title: "API edge", + detail: runtime.endpoints.api, + tone: "source" + }, + { + title: "Runtime route", + detail: runtime.serviceRoute.join(" -> "), + tone: "source" + } + ]; + + if (live) { + diagnostics.push( + { + title: "/health/live", + detail: live.healthLive.ok ? `HTTP ${live.healthLive.status} ${live.healthLive.data?.status ?? "ok"}` : live.healthLive.error, + tone: live.healthLive.ok ? live.healthLive.data?.status ?? "dev-live" : "blocked" + }, + { + title: "/v1", + detail: live.restIndex.ok ? `HTTP ${live.restIndex.status} ${live.restIndex.data?.status ?? "ok"}` : live.restIndex.error, + tone: live.restIndex.ok ? live.restIndex.data?.status ?? "dev-live" : "blocked" + }, + { + title: "system.health", + detail: live.health.ok ? `RPC ${live.health.data?.status ?? "ok"}` : live.health.error, + tone: live.health.ok ? live.health.data?.status ?? "dev-live" : "blocked" + }, + { + title: "audit.event.query", + detail: live.audit.ok ? `${live.audit.data?.count ?? live.audit.data?.events?.length ?? 0} records` : live.audit.error, + tone: live.audit.ok ? "dev-live" : "blocked" + } + ); + } else { + diagnostics.push( + { + title: "/health/live", + detail: "probing same-origin web/API health", + tone: "dev-live" + }, + { + title: "/v1 + /json-rpc", + detail: "read-only diagnostics pending", + tone: "source" + } + ); + } + + replaceChildren(el.diagnosticsList, ...diagnostics.map(infoCard)); } function renderGateDiagnostics() { - appendMetricGrid(el.metaGrid, [ - ["Environment", gateSummary.environment], - ["Frontend", runtime.endpoints.frontend], - ["Endpoint", gateSummary.endpoint], - ["Namespace", gateSummary.namespace], - ["Report commit", gateSummary.reportCommitId], - ["Route", runtime.serviceRoute.join(" -> ")], - ["Gate report", gateSummary.generatedFrom.gateReport] + appendMetricGrid(el.gateMetadata, [ + ["Environment", gateSummary.environment, "source"], + ["Frontend", runtime.endpoints.frontend, "source"], + ["API", runtime.endpoints.api, "source"], + ["Gate", gateSummary.gateStatus, gateSummary.gateStatus], + ["Dry-run", gateSummary.dryRun.status, "dry-run"], + ["Report", shortHash(gateSummary.reportCommitId), "source"] ]); - el.milestoneGrid.replaceChildren(); - for (const milestone of gateSummary.milestones) { - const item = document.createElement("article"); - item.className = "milestone"; - const head = document.createElement("div"); - head.className = "milestone-head"; - appendText(head, "strong", milestone.id); - head.append(badge(milestone.status, milestone.status)); - item.append(head); - appendText(item, "p", milestone.summary); - appendText(item, "small", `${milestone.commandCount} commands / ${milestone.evidenceCount} evidence lines`); - el.milestoneGrid.append(item); - } + replaceChildren( + el.milestoneGrid, + ...gateSummary.milestones.map((milestone) => { + const item = document.createElement("article"); + item.className = "milestone-card"; + const header = document.createElement("div"); + header.className = "panel-title-row"; + header.append(textSpan(milestone.id, "milestone-id"), badge(milestone.status, milestone.status)); + item.append(header); + item.append(textSpan(milestone.summary, "milestone-summary")); + item.append(textSpan(`${milestone.commandCount} commands / ${milestone.evidenceCount} evidence`, "milestone-meta")); + return item; + }) + ); el.healthBody.replaceChildren(); for (const row of gateSummary.health) { const tr = document.createElement("tr"); tr.append(cell(row.component)); tr.append(cell(row.serviceId, "mono")); - const statusCell = document.createElement("td"); - statusCell.append(badge(row.status, row.status)); - tr.append(statusCell); + const status = document.createElement("td"); + status.append(badge(row.status, row.status)); + tr.append(status); tr.append(cell(row.observedBy)); tr.append(cell(row.endpoint ?? gateSummary.endpoint, "mono wrap")); el.healthBody.append(tr); } - el.blockerList.replaceChildren(); - for (const blocker of gateSummary.blockers) { - const li = document.createElement("li"); - const head = document.createElement("div"); - head.className = "blocker-head"; - head.append(badge(blocker.type, "blocked")); - appendText(head, "strong", blocker.scope); - li.append(head); - appendText(li, "p", blocker.summary); - el.blockerList.append(li); - } - - el.commandList.replaceChildren(); - for (const command of [ - "node web/hwlab-cloud-web/scripts/m3-readonly-contract.mjs", - "node web/hwlab-cloud-web/scripts/check.mjs", - "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp", - "node scripts/m5-mvp-e2e-dry-run.mjs" - ]) { - appendText(el.commandList, "li", command, "mono"); - } + replaceChildren( + el.validationList, + ...[ + "node --check web/hwlab-cloud-web/app.mjs", + "node --check web/hwlab-cloud-web/scripts/check.mjs", + "node web/hwlab-cloud-web/scripts/check.mjs", + "node scripts/l6-cli-web-smoke.mjs", + "git diff --check" + ].map((command) => { + const li = document.createElement("li"); + li.textContent = command; + return li; + }) + ); } -function m3Status() { - return gateSummary.milestones.find((milestone) => milestone.id === "M3")?.status ?? "unknown"; +function renderMethodList(methods, tone) { + replaceChildren(el.methodList, ...methods.map((method) => badge(method, tone === "live" ? "dev-live" : tone))); +} + +function blockerCard(blocker) { + return infoCard({ + title: blocker.scope, + detail: blocker.summary, + tone: blocker.status === "open" ? "blocked" : blocker.status + }); +} + +function messageCard(message) { + const article = document.createElement("article"); + article.className = `message-card message-${toneClass(message.role)}`; + article.append(textSpan(message.role, "message-role")); + article.append(textSpan(message.title, "message-title")); + article.append(textSpan(message.text, "message-copy")); + return article; +} + +function statusCard(item) { + const article = document.createElement("article"); + article.className = "status-card"; + article.append(textSpan(item.title, "metric-label")); + article.append(textSpan(String(item.value), `status-value tone-${toneClass(item.tone)}`)); + article.append(textSpan(item.meta, "status-meta")); + return article; +} + +function infoCard(item) { + const article = document.createElement("article"); + article.className = "info-card"; + article.append(badge(item.tone, item.tone)); + const body = document.createElement("div"); + body.append(textSpan(item.title, "info-title")); + body.append(textSpan(item.detail, "info-detail")); + article.append(body); + return article; +} + +function stateTag(label, detail) { + const span = document.createElement("span"); + span.className = `state-tag tone-${toneClass(label)}`; + span.textContent = `${label} ${detail}`; + return span; +} + +function statusLight(tone) { + const span = document.createElement("span"); + span.className = `tree-light tone-bg-${toneClass(tone)}`; + return span; +} + +function badge(text, tone = text) { + const span = document.createElement("span"); + span.className = `badge tone-${toneClass(tone)}`; + span.textContent = text ?? "n/a"; + return span; +} + +function appendMetricGrid(parent, entries) { + replaceChildren( + parent, + ...entries.map(([label, value, tone]) => { + const item = document.createElement("div"); + item.className = "metric"; + item.append(textSpan(label, "metric-label"), textSpan(String(value ?? "n/a"), `metric-value tone-${toneClass(tone ?? "source")}`)); + return item; + }) + ); +} + +function textSpan(text, className) { + const span = document.createElement("span"); + span.textContent = text ?? "n/a"; + if (className) span.className = className; + return span; +} + +function cell(text, className) { + const td = document.createElement("td"); + td.textContent = text ?? "n/a"; + if (className) td.className = className; + return td; } function runtimeSummaryFrom(live) { - return live.health.data?.runtime ?? live.adapter.data?.runtime ?? live.restIndex.data?.runtime ?? null; + return live.health.data?.runtime ?? live.healthLive.data?.runtime ?? live.adapter.data?.runtime ?? live.restIndex.data?.runtime ?? null; } function methodsFrom(live) { @@ -522,6 +755,10 @@ function methodsFrom(live) { return []; } +function dbReadyFrom(live) { + return live.healthLive.data?.db?.ready ?? live.health.data?.db?.ready ?? live.restIndex.data?.db?.ready ?? null; +} + function probeErrors(probes) { return probes.filter((probe) => !probe.ok).map((probe) => `${probe.method ?? probe.path}: ${probe.error}`); } @@ -531,75 +768,24 @@ function messageFromPayload(payload) { return payload.error?.message || payload.error?.code || payload.message || JSON.stringify(payload).slice(0, 160); } -function valueOrUnavailable(value) { - return value === undefined || value === null ? "unavailable" : value; +function valueOrUnavailable(liveValue, sourceValue) { + if (liveValue !== undefined && liveValue !== null) return liveValue; + if (sourceValue !== undefined && sourceValue !== null) return `${sourceValue} source`; + return "unavailable"; } function shortTime(value) { return new Date(value).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z"); } -function appendText(parent, tagName, text, className) { - const element = document.createElement(tagName); - element.textContent = text; - if (className) { - element.className = className; - } - parent.append(element); - return element; -} - -function appendMetricGrid(parent, entries) { - parent.replaceChildren(); - for (const entry of entries) { - const [label, value, tone] = entry; - const item = document.createElement("div"); - item.className = "metric"; - appendText(item, "span", label, "metric-label"); - const strong = appendText(item, "strong", String(value ?? "n/a"), "metric-value"); - if (tone) { - strong.classList.add(`tone-${toneClass(tone)}`); - } - parent.append(item); - } -} - -function badge(text, tone = text) { - const span = document.createElement("span"); - span.className = `badge tone-${toneClass(tone)}`; - span.textContent = text ?? "n/a"; - return span; +function shortHash(value) { + return String(value ?? "unknown").slice(0, 7); } function toneClass(tone) { return String(tone ?? "unknown").replace(/[^a-z0-9_-]/gi, "-").toLowerCase(); } -function cell(text, className) { - const td = document.createElement("td"); - td.textContent = text ?? "n/a"; - if (className) { - td.className = className; - } - return td; -} - -function rowNotice(tbody, colSpan, text, tone = "warn") { - tbody.replaceChildren(); - const tr = document.createElement("tr"); - const td = cell(text, `notice-cell tone-${toneClass(tone)}`); - td.colSpan = colSpan; - tr.append(td); - tbody.append(tr); -} - -function emptyNotice(text) { - const div = document.createElement("div"); - div.className = "empty"; - div.textContent = text; - return div; -} - -function replaceChildren(parent, child) { - parent.replaceChildren(child); +function replaceChildren(parent, ...children) { + parent.replaceChildren(...children.filter((child) => child && typeof child !== "string")); } diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 0f32e236..d390d5c6 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -3,287 +3,221 @@ - HWLAB DEV Console + HWLAB Cloud Workbench -
-
-
-

HWLAB Cloud Web / DEV

-

M3 Diagnostics Console

-

Read-only runtime observation for cloud-api health, M3 topology contracts, route, patch-panel blockers, and evidence summaries.

-
-
- Live API - probing - Checking /v1 and /json-rpc from this origin. -
-
+
+ - - -
-
+
-
-
-
-
-

Live Method Registry

-

Waiting for cloud-api adapter description.

-
-
-
-
-
-

Runtime Summary

-

Waiting for cloud-api runtime summary.

-
-
-
-
-
+ blocked + +
+ SOURCE loading + DRY-RUN loading + DEV-LIVE probing + BLOCKED pending +
+
+
+

Resource Tree

+ 0 +
+ +
+
+
+

Current Blockers

+ 0 +
+
+
+ -
diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index abaf7756..0dff242c 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -22,29 +22,39 @@ const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8"); const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/dev-artifact-publish.mjs"), "utf8"); const frontendSource = `${html}\n${app}\n${artifactPublisher}`; -assert.match(html, /HWLAB DEV Console/); -assert.match(html, /M3 Diagnostics Console/); -assert.match(html, /Read-only runtime observation/); -for (const moduleName of [ - "Overview", - "Topology / Patch Panel", - "M3 Evidence", - "Agent Sessions", - "Evidence / Audit", - "Gate / Diagnostics" +assert.match(html, /HWLAB Cloud Workbench/); +assert.match(html, /Agent Conversation \/ Trace Workspace/); +assert.match(html, /BOX-SIMU \/ Gateway-SIMU \/ Patch Panel/); +assert.match(html, /Gate \/ Diagnostics/); +for (const workbenchElement of [ + "activity-rail", + "explorer", + "resource-tree", + "conversation-list", + "trace-list", + "right-sidebar", + "command-form", + "hardware-list", + "control-list", + "wiring-list", + "records-list", + "diagnostics-list" ]) { - assert.match(html, new RegExp(moduleName.replaceAll("/", "\\/"))); + assert.match(html, new RegExp(workbenchElement)); } -for (const viewId of ["overview", "topology", "hardware", "agents", "evidence", "gate"]) { +for (const viewId of ["workspace", "gate"]) { assert.match(html, new RegExp(`data-view="${viewId}"`)); } assert.match(html, /live-status/); -assert.match(html, /method-grid/); -assert.match(html, /runtime-metrics/); -assert.match(html, /operation-body/); -assert.match(html, /audit-body/); -assert.match(html, /evidence-body/); +assert.match(html, /method-list/); +assert.match(html, /source-strip/); +assert.match(html, /SOURCE loading/); +assert.match(html, /DRY-RUN loading/); +assert.match(html, /DEV-LIVE probing/); +assert.match(html, /BLOCKED pending/); +assert.doesNotMatch(html, /M3 Diagnostics Console/); assert.match(app, /fetchJson\("\/v1"\)/); +assert.match(app, /fetchJson\("\/health\/live"\)/); assert.match(app, /callRpc\("system\.health"\)/); assert.match(app, /callRpc\("cloud\.adapter\.describe"\)/); assert.match(app, /callRpc\("audit\.event\.query"/); @@ -90,4 +100,4 @@ assert.equal(gateSummary.safety.allowNetwork, false); runCloudWebM3ReadonlyContract(); -console.log("hwlab-cloud-web check ok: read-only M3 diagnostics contract is present"); +console.log("hwlab-cloud-web check ok: workbench shell is present and read-only diagnostics contract is preserved"); diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index 2725d7f6..68190ba1 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -1,20 +1,22 @@ :root { color-scheme: dark; - --bg: #11120f; - --surface: #181914; - --surface-2: #202119; - --surface-3: #28291f; - --surface-4: #313226; - --text: #f2f0e8; - --muted: #b8b3a5; - --line: #464333; - --line-strong: #6e674c; - --accent: #e8bf50; - --accent-2: #7dcfc4; - --ok: #83c77b; - --warn: #e1a84b; - --bad: #f06d5f; - --info: #8ab4df; + --bg: #0f1110; + --rail: #141614; + --surface: #181b18; + --surface-2: #1f231f; + --surface-3: #282e28; + --surface-hot: #342f1d; + --text: #ece8dc; + --muted: #a9aa9e; + --dim: #73776d; + --line: #353a33; + --line-strong: #555d52; + --accent: #d4ad43; + --accent-2: #6fb7a9; + --ok: #7fbe74; + --warn: #dfaa55; + --bad: #e76e5e; + --info: #7ea6c9; --mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; --body: "Aptos", "Segoe UI", system-ui, sans-serif; } @@ -28,352 +30,677 @@ body { margin: 0; min-height: 100%; background: - linear-gradient(90deg, rgba(232, 191, 80, 0.05) 1px, transparent 1px), - linear-gradient(0deg, rgba(125, 207, 196, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(212, 173, 67, 0.055) 1px, transparent 1px), + linear-gradient(0deg, rgba(111, 183, 169, 0.04) 1px, transparent 1px), var(--bg); - background-size: 44px 44px; + background-size: 36px 36px; color: var(--text); - font: 13px/1.45 var(--body); + font: 12px/1.4 var(--body); } -body { - padding: 18px; +button, +input { + font: inherit; } -.shell { - max-width: 1440px; - margin: 0 auto; - display: grid; - gap: 12px; -} - -.masthead, -.module-nav, -.panel, -.section-head { - background: rgba(24, 25, 20, 0.96); - border: 1px solid var(--line); -} - -.masthead { - min-height: 112px; - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); - gap: 16px; - align-items: stretch; - padding: 16px; -} - -.identity { - display: grid; - align-content: center; - gap: 6px; -} - -.eyebrow { - margin: 0; - color: var(--accent-2); - font-size: 11px; - letter-spacing: 0; - text-transform: uppercase; +button { + color: inherit; } h1, h2, h3, p, -dl, ol, -ul, -table { +ul { margin: 0; } +.workbench-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 48px 292px minmax(460px, 1fr) 366px; + grid-template-rows: 1fr; + background: rgba(15, 17, 16, 0.88); +} + +.activity-rail, +.explorer, +.right-sidebar, +.center-workspace { + min-width: 0; + min-height: 0; +} + +.activity-rail { + display: grid; + grid-template-rows: repeat(3, 42px) 1fr 42px; + gap: 6px; + padding: 8px 6px; + background: var(--rail); + border-right: 1px solid var(--line); +} + +.rail-spacer { + min-height: 20px; +} + +.rail-button { + width: 36px; + min-height: 36px; + border: 1px solid transparent; + background: transparent; + color: var(--muted); + font-family: var(--mono); + font-size: 11px; + font-weight: 760; + cursor: pointer; +} + +.rail-button:hover, +.rail-button:focus-visible, +.rail-button.active { + border-color: var(--line-strong); + background: var(--surface-2); + color: var(--text); + outline: 0; +} + +.rail-button.active { + box-shadow: inset 3px 0 0 var(--accent); +} + +.explorer { + display: grid; + grid-template-rows: auto auto minmax(0, 1fr) auto; + gap: 8px; + padding: 10px; + background: rgba(22, 25, 22, 0.98); + border-right: 1px solid var(--line); + overflow: hidden; +} + +.explorer-collapsed { + grid-template-columns: 48px 0 minmax(460px, 1fr) 366px; +} + +.explorer-collapsed .explorer { + padding: 0; + border-right: 0; + overflow: hidden; +} + +.explorer-collapsed .explorer > * { + display: none; +} + +.explorer-head, +.topbar, +.hardware-status, +.side-workspace, +.workspace-panel { + background: rgba(24, 27, 24, 0.97); + border: 1px solid var(--line); +} + +.explorer-head { + min-height: 78px; + display: flex; + justify-content: space-between; + gap: 10px; + align-items: start; + padding: 12px; +} + +.eyebrow { + color: var(--accent-2); + font-size: 10px; + font-weight: 740; + letter-spacing: 0; + text-transform: uppercase; +} + h1 { - font-size: 28px; - line-height: 1.08; - font-weight: 780; + margin-top: 3px; + font-size: 20px; + line-height: 1.1; } h2 { - font-size: 18px; + font-size: 13px; line-height: 1.2; } h3 { - font-size: 14px; - line-height: 1.2; + font-size: 12px; } -.lede, -.section-head p, -.panel-head p, -.live-card small { +.status-dot, +.state-tag, +.badge { + display: inline-flex; + align-items: center; + width: fit-content; + max-width: 100%; + min-height: 22px; + padding: 3px 7px; + border: 1px solid var(--line); + background: var(--surface-2); + font-family: var(--mono); + font-size: 10px; + font-weight: 760; + text-transform: uppercase; + overflow-wrap: anywhere; +} + +.source-strip { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; +} + +.source-strip .state-tag { + width: 100%; + justify-content: center; + min-width: 0; +} + +.tree-panel { + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 8px; + padding: 10px; + background: var(--surface); + border: 1px solid var(--line); + overflow: hidden; +} + +.blockers-panel { + max-height: 230px; +} + +.panel-title-row { + display: flex; + justify-content: space-between; + gap: 10px; + align-items: center; + min-width: 0; +} + +.panel-title-row > div { + min-width: 0; +} + +.panel-title-row p, +.panel-copy { color: var(--muted); } -.lede { - max-width: 780px; +.resource-tree, +.compact-list, +.conversation-list, +.trace-list, +.hardware-list { + min-height: 0; + overflow: auto; } -.live-card { +.resource-tree { + display: grid; + align-content: start; + gap: 6px; +} + +.resource-tree details { + border: 1px solid var(--line); + background: var(--surface-2); +} + +.resource-tree summary, +.tree-row { + width: 100%; + min-height: 31px; + display: grid; + grid-template-columns: 8px minmax(0, 1fr); + gap: 7px; + align-items: center; + padding: 7px 8px; + border: 0; + background: transparent; + text-align: left; +} + +.resource-tree summary { + cursor: pointer; +} + +.tree-row { + padding-left: 22px; + border-top: 1px solid var(--line); + cursor: pointer; +} + +.tree-row:hover { + background: rgba(212, 173, 67, 0.06); +} + +.tree-label, +.tree-meta { + display: block; + min-width: 0; + overflow-wrap: anywhere; +} + +.tree-label { + color: var(--text); + font-family: var(--mono); + font-size: 11px; +} + +.tree-meta { + grid-column: 2; + color: var(--muted); + font-size: 11px; +} + +.tree-light { + width: 8px; + height: 8px; + background: var(--dim); +} + +.center-workspace { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + overflow: hidden; +} + +.topbar { + min-height: 82px; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(260px, 360px); + gap: 12px; + align-items: stretch; + padding: 12px; + border-left: 0; + border-right: 0; +} + +.topbar-main { display: grid; align-content: center; - gap: 5px; - padding: 14px; + gap: 4px; + min-width: 0; +} + +.topbar-main h2 { + font-size: 19px; +} + +.route-path { + max-width: 100%; + color: var(--muted); + font-family: var(--mono); + font-size: 11px; + overflow-wrap: anywhere; +} + +.probe-card { + display: grid; + align-content: center; + gap: 4px; + padding: 10px 12px; background: var(--surface-2); border-left: 4px solid var(--accent); } -.live-card .label { +.probe-card strong { + font-size: 17px; + line-height: 1; + text-transform: uppercase; +} + +.probe-card small { color: var(--muted); - font-size: 11px; - text-transform: uppercase; -} - -.live-card strong { - font-size: 22px; - line-height: 1.1; - text-transform: uppercase; -} - -.module-nav { - display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 1px; - padding: 1px; - position: sticky; - top: 0; - z-index: 5; -} - -.module-nav a { - min-height: 42px; - display: flex; - align-items: center; - justify-content: center; - padding: 8px 10px; - background: var(--surface-2); - color: var(--text); - text-align: center; - text-decoration: none; overflow-wrap: anywhere; } -.module-nav a:hover, -.module-nav a:focus-visible { - background: var(--surface-3); - outline: 0; -} - -.module-nav a.active { - background: #35311f; - color: #fff4cd; - box-shadow: inset 0 -3px 0 var(--accent); -} - .view { - display: grid; - gap: 12px; + min-height: 0; + overflow: auto; } .view[hidden] { display: none; } -.section-head { - display: flex; - justify-content: space-between; - gap: 18px; - align-items: end; - padding: 14px 16px; +.workbench-view { + padding: 10px; } -.section-head p { - max-width: 620px; - text-align: right; +.conversation-column { + min-height: 100%; + display: grid; + grid-template-rows: minmax(280px, 1fr) minmax(170px, 0.45fr); + gap: 10px; } -.panel { +.workspace-panel { min-width: 0; - padding: 14px 16px; -} - -.panel.compact { - padding: 12px 14px; -} - -.panel-head { + min-height: 0; display: grid; - gap: 4px; - margin-bottom: 12px; + gap: 10px; + padding: 12px; } -.split { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 12px; +.conversation-panel, +.trace-panel { + grid-template-rows: auto minmax(0, 1fr); } -.metrics { +.conversation-list, +.compact-list, +.trace-list { display: grid; + align-content: start; gap: 8px; } -.metrics.six { - grid-template-columns: repeat(6, minmax(0, 1fr)); -} - -.metrics.four { - grid-template-columns: repeat(4, minmax(0, 1fr)); -} - -.metrics.two { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - +.message-card, +.info-card, +.trace-item, .metric, -.milestone { +.milestone-card, +.status-card { min-width: 0; background: var(--surface-2); border: 1px solid var(--line); - padding: 9px 10px; +} + +.message-card { + display: grid; + grid-template-columns: 82px minmax(0, 1fr); + gap: 6px 12px; + padding: 10px; +} + +.message-role { + grid-row: span 2; + color: var(--accent); + font-family: var(--mono); + font-size: 10px; + font-weight: 760; + text-transform: uppercase; +} + +.message-title { + color: var(--text); + font-weight: 760; +} + +.message-copy { + color: var(--muted); + overflow-wrap: anywhere; +} + +.message-blocker { + border-color: rgba(231, 110, 94, 0.48); +} + +.trace-item { + display: grid; + grid-template-columns: 38px minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + padding: 8px 10px; +} + +.trace-order { + color: var(--accent); + font-family: var(--mono); + font-weight: 760; +} + +.trace-title, +.info-title, +.milestone-id { + display: block; + color: var(--text); + font-weight: 760; + overflow-wrap: anywhere; +} + +.trace-meta, +.info-detail, +.milestone-meta, +.milestone-summary, +.status-meta { + display: block; + color: var(--muted); + overflow-wrap: anywhere; +} + +.command-bar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 8px; + padding: 10px; + background: rgba(20, 22, 20, 0.98); + border-top: 1px solid var(--line); +} + +.input-shell { + min-width: 0; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 8px; + align-items: center; + padding: 0 10px; + background: var(--surface); + border: 1px solid var(--line-strong); +} + +.prompt-mark { + color: var(--accent); + font-family: var(--mono); + font-weight: 760; +} + +.input-shell input { + width: 100%; + min-height: 38px; + border: 0; + outline: 0; + background: transparent; + color: var(--text); +} + +.input-shell input::placeholder { + color: var(--dim); +} + +.command-button { + min-height: 38px; + padding: 0 12px; + border: 1px solid var(--accent); + background: var(--surface-hot); + color: var(--text); + font-weight: 760; + cursor: pointer; +} + +.command-button.secondary { + border-color: var(--line-strong); + background: var(--surface-2); +} + +.right-sidebar { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 10px; + padding: 10px; + background: rgba(18, 20, 18, 0.98); + border-left: 1px solid var(--line); + overflow: hidden; +} + +.hardware-status { + display: grid; + gap: 10px; + padding: 12px; +} + +.hardware-list { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.status-card { + display: grid; + gap: 5px; + padding: 9px; +} + +.status-value { + font-size: 18px; + font-weight: 800; + line-height: 1; + overflow-wrap: anywhere; +} + +.side-workspace { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + overflow: hidden; +} + +.side-tabs { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + border-bottom: 1px solid var(--line); +} + +.side-tab { + min-height: 34px; + padding: 6px 4px; + border: 0; + border-right: 1px solid var(--line); + background: var(--surface); + color: var(--muted); + font-size: 11px; + font-weight: 760; + cursor: pointer; +} + +.side-tab:last-child { + border-right: 0; +} + +.side-tab:hover, +.side-tab:focus-visible, +.side-tab.active { + background: var(--surface-2); + color: var(--text); + outline: 0; +} + +.side-tab.active { + box-shadow: inset 0 -3px 0 var(--accent); +} + +.side-panel { + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + gap: 10px; + padding: 12px; + overflow: hidden; +} + +.side-panel[hidden] { + display: none; +} + +.info-card { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 8px; + align-items: start; + padding: 9px; +} + +.readonly-rpc { + display: grid; + gap: 7px; + padding-top: 10px; + border-top: 1px solid var(--line); +} + +.rpc-chip-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.gate-view { + display: grid; + gap: 10px; + padding: 10px; +} + +.gate-grid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 8px; +} + +.gate-split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.metric { + display: grid; + gap: 4px; + padding: 9px; } .metric-label { - display: block; color: var(--muted); - font-size: 11px; - line-height: 1.2; + font-size: 10px; + font-weight: 760; text-transform: uppercase; } .metric-value { - display: block; - margin-top: 4px; - font-size: 13px; - line-height: 1.25; - overflow-wrap: anywhere; -} - -.method-grid { - display: flex; - flex-wrap: wrap; - gap: 7px; -} - -.method-pill { - max-width: 100%; - min-height: 28px; - display: inline-flex; - align-items: center; - padding: 4px 8px; - border: 1px solid var(--line); - background: var(--surface-2); + color: var(--text); font-family: var(--mono); - font-size: 12px; overflow-wrap: anywhere; } -.method-pill.read { - border-color: rgba(125, 207, 196, 0.45); -} - -.milestones { +.milestone-grid { display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } -.milestone { +.milestone-card { display: grid; - gap: 7px; - min-height: 118px; -} - -.milestone-head, -.blocker-head { - display: flex; - justify-content: space-between; - align-items: center; gap: 8px; -} - -.milestone p, -.blockers p { - color: var(--text); - overflow-wrap: anywhere; -} - -.milestone small { - color: var(--muted); -} - -.badge { - display: inline-flex; - align-items: center; - width: fit-content; - max-width: 100%; - min-height: 24px; - padding: 3px 8px; - border: 1px solid var(--line); - background: var(--surface-3); - color: var(--text); - font-size: 11px; - font-weight: 730; - text-transform: uppercase; - overflow-wrap: anywhere; -} - -.tone-pass, -.tone-healthy, -.tone-connected, -.tone-completed, -.tone-succeeded, -.tone-available, -.tone-active, -.tone-accepted { - color: var(--ok); -} - -.tone-blocked, -.tone-degraded, -.tone-network_blocker, -.tone-failed, -.tone-rejected, -.tone-unavailable { - color: var(--bad); -} - -.tone-not_run, -.tone-warn, -.tone-warning, -.tone-probing, -.tone-false { - color: var(--warn); -} - -.tone-live, -.tone-recorded { - color: var(--info); -} - -.tone-muted { - color: var(--muted); + padding: 10px; } .table-wrap { - overflow-x: auto; + overflow: auto; border: 1px solid var(--line); } -.compact-table table { - min-width: 620px; -} - table { width: 100%; - min-width: 900px; + min-width: 860px; border-collapse: collapse; background: var(--surface-2); } @@ -382,14 +709,14 @@ th, td { text-align: left; vertical-align: top; - padding: 8px 9px; + padding: 8px; border-bottom: 1px solid var(--line); } th { color: var(--muted); - font-size: 11px; - font-weight: 730; + font-size: 10px; + font-weight: 760; text-transform: uppercase; } @@ -402,9 +729,26 @@ tbody tr:last-child td { border-bottom: 0; } +.command-list { + display: grid; + gap: 8px; + padding: 0; + list-style: none; +} + +.command-list li { + min-width: 0; + padding: 9px; + background: var(--surface-2); + border: 1px solid var(--line); + color: var(--muted); + font-family: var(--mono); + overflow-wrap: anywhere; +} + .mono { font-family: var(--mono); - font-size: 12px; + font-size: 11px; } .wrap { @@ -412,92 +756,154 @@ tbody tr:last-child td { word-break: break-word; } -.empty, -.notice-cell { - color: var(--muted); - background: var(--surface-2); -} - -.empty { - width: 100%; - padding: 10px; - border: 1px solid var(--line); -} - .danger { - border-color: #875148; + border-color: rgba(231, 110, 94, 0.55); } -.blockers, -.commands { - display: grid; - gap: 8px; - padding-left: 0; - list-style: none; +.tone-pass, +.tone-healthy, +.tone-connected, +.tone-completed, +.tone-succeeded, +.tone-available, +.tone-active, +.tone-accepted, +.tone-ok { + color: var(--ok); } -.blockers li, -.commands li { - min-width: 0; - padding: 9px 10px; - background: var(--surface-2); - border: 1px solid var(--line); - overflow-wrap: anywhere; +.tone-blocked, +.tone-degraded, +.tone-network_blocker, +.tone-runtime_blocker, +.tone-failed, +.tone-rejected, +.tone-unavailable { + color: var(--bad); } -.commands { - counter-reset: command; +.tone-dry-run, +.tone-not_run, +.tone-warn, +.tone-warning, +.tone-probing, +.tone-false { + color: var(--warn); } -.commands li { - counter-increment: command; +.tone-dev-live, +.tone-live, +.tone-recorded { + color: var(--info); } -.commands li::before { - content: counter(command) ". "; - color: var(--accent); - font-family: var(--body); - font-weight: 730; +.tone-source, +.tone-muted { + color: var(--muted); } -@media (max-width: 1180px) { - .module-nav, - .metrics.six, - .milestones { - grid-template-columns: repeat(3, minmax(0, 1fr)); +.tone-bg-pass, +.tone-bg-healthy, +.tone-bg-connected, +.tone-bg-completed, +.tone-bg-succeeded, +.tone-bg-available, +.tone-bg-active, +.tone-bg-accepted, +.tone-bg-ok { + background: var(--ok); +} + +.tone-bg-blocked, +.tone-bg-degraded, +.tone-bg-network_blocker, +.tone-bg-runtime_blocker, +.tone-bg-failed, +.tone-bg-rejected, +.tone-bg-unavailable { + background: var(--bad); +} + +.tone-bg-dry-run, +.tone-bg-not_run, +.tone-bg-warn, +.tone-bg-warning, +.tone-bg-probing, +.tone-bg-false { + background: var(--warn); +} + +.tone-bg-dev-live, +.tone-bg-live, +.tone-bg-recorded { + background: var(--info); +} + +.tone-bg-source, +.tone-bg-muted { + background: var(--dim); +} + +@media (max-width: 1240px) { + .workbench-shell { + grid-template-columns: 48px 260px minmax(420px, 1fr); } - .metrics.four { - grid-template-columns: repeat(2, minmax(0, 1fr)); + .right-sidebar { + grid-column: 2 / 4; + grid-row: 2; + min-height: 420px; + border-top: 1px solid var(--line); + border-left: 0; + } + + .gate-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); } } @media (max-width: 860px) { - body { - padding: 10px; + .workbench-shell, + .explorer-collapsed { + grid-template-columns: 44px minmax(0, 1fr); } - .masthead, - .split, - .section-head { + .explorer { + grid-column: 2; + } + + .center-workspace, + .right-sidebar { + grid-column: 1 / 3; + } + + .center-workspace { + grid-row: 2; + } + + .right-sidebar { + grid-row: 3; + } + + .explorer-collapsed .explorer { + display: none; + } + + .topbar, + .command-bar, + .gate-split { grid-template-columns: 1fr; } - .masthead, - .section-head { - display: grid; - align-items: start; + .hardware-list, + .milestone-grid, + .gate-grid, + .source-strip { + grid-template-columns: 1fr; } - .section-head p { - text-align: left; - } - - .module-nav, - .metrics.six, - .metrics.four, - .metrics.two, - .milestones { + .message-card, + .trace-item { grid-template-columns: 1fr; } }