From 83fd2ce3b025440c7c8848307c246d2dc8b9b7fd Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 21 Jun 2026 15:52:15 +0000 Subject: [PATCH] fix: render web observe status as table --- scripts/cli.ts | 5 + scripts/src/hwlab-node-impl.ts | 106 +++++++++++++++++++- scripts/src/hwlab-node.ts | 3 +- scripts/src/platform-infra-observability.ts | 5 +- 4 files changed, 114 insertions(+), 5 deletions(-) diff --git a/scripts/cli.ts b/scripts/cli.ts index 273c403a..46a6c479 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -321,6 +321,11 @@ async function main(): Promise { const { runHwlabNodeCommand } = await import("./src/hwlab-node"); const result = await runHwlabNodeCommand(readConfig(), args.slice(2)); const ok = (result as { ok?: unknown }).ok !== false; + if (isRenderedCliResult(result)) { + emitText(result.renderedText, result.command || commandName); + if (!ok) process.exitCode = 1; + return; + } emitJson(commandName, result, ok); if (!ok) process.exitCode = 1; return; diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index c894d5dc..664751ff 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -15,6 +15,7 @@ import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from import { compactWebProbeResult, compactWebProbeScriptResult } from "./hwlab-node-web-probe-summary"; import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "./hwlab-node-observability-promql"; import { runDelegatedHwlabNodeCommand, type DelegatedNodeDomain } from "./hwlab-node-transport"; +import type { RenderedCliResult } from "./output"; type SecretAction = "status" | "ensure" | "cleanup-owned-postgres" | "cleanup-obsolete"; type SecretPreset = "openfga" | "master-server-admin-api-key" | "bootstrap-admin" | "code-agent-provider" | "cloud-api-db" | "owned-postgres-cleanup" | "obsolete-secret-cleanup"; @@ -289,7 +290,7 @@ const CODE_AGENT_PROVIDER_SOURCE_NAMESPACE = "hwlab-v02"; const CODE_AGENT_PROVIDER_SOURCE_SECRET = "hwlab-v02-code-agent-provider"; const HWLAB_CI_NAMESPACE = "hwlab-ci"; -export async function runHwlabNodeCommand(_config: Config, args: string[]): Promise> { +export async function runHwlabNodeCommand(_config: Config, args: string[]): Promise | RenderedCliResult> { if (args.length === 0) return hwlabNodeHelp(); const [domain] = args; if (domain === "control-plane" && args[1] === "infra") { @@ -7039,7 +7040,7 @@ function runNodeWebProbeObserveStart( }; } -function runNodeWebProbeObserveStatus(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record { +function runNodeWebProbeObserveStatus(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record | RenderedCliResult { const script = [ "set -eu", nodeWebObserveResolveStateDirShell(options), @@ -7062,7 +7063,7 @@ function runNodeWebProbeObserveStatus(options: NodeWebProbeObserveOptions, spec: const index = ok && observerId !== null && options.stateDir !== null ? upsertWebObserveIndexEntry(webObserveIndexEntryFromOptions(options, spec, observerId, status)) : null; - return { + return withWebObserveStatusRendered({ ok, status: ok ? "observed" : "blocked", command: webObserveCommandLabel("status", options), @@ -7076,9 +7077,108 @@ function runNodeWebProbeObserveStatus(options: NodeWebProbeObserveOptions, spec: next: observerId === null ? null : webObserveNextCommands(observerId), result: compactCommandResult(result), valuesRedacted: true, + }); +} + +function withWebObserveStatusRendered(value: Record): RenderedCliResult { + return { + ok: value.ok !== false, + command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe status", + contentType: "text/plain", + renderedText: renderWebObserveStatusTable(value), }; } +function renderWebObserveStatusTable(value: Record): string { + const observer = record(value.observer); + const manifest = record(observer?.manifest); + const heartbeat = record(observer?.heartbeat); + const tails = record(observer?.tails); + const samples = Array.isArray(tails?.samples) ? tails.samples.map(record).filter((item): item is Record => item !== null).slice(-5) : []; + const controls = Array.isArray(tails?.control) ? tails.control.map(record).filter((item): item is Record => item !== null).slice(-5) : []; + const network = Array.isArray(tails?.network) + ? tails.network.map(record).filter((item): item is Record => item !== null).filter((item) => { + const status = typeof item.status === "number" ? item.status : null; + return status === null || status >= 400 || item.failure !== null && item.failure !== undefined; + }).slice(-5) + : []; + const next = record(value.next); + const lines = [ + `hwlab nodes web-probe observe status (${webObserveText(value.status)})`, + "", + webObserveTable(["ID", "NODE", "LANE", "ALIVE", "PID", "SAMPLE", "UPDATED", "TARGET"], [[ + webObserveText(value.id), + webObserveText(value.node), + webObserveText(value.lane), + webObserveText(observer?.processAlive), + webObserveText(observer?.pid), + webObserveText(heartbeat?.sampleSeq), + webObserveShort(webObserveText(heartbeat?.updatedAt ?? heartbeat?.lastSampleAt), 24), + webObserveShort(`${webObserveText(manifest?.baseUrl)}${webObserveText(manifest?.targetPath) === "-" ? "" : webObserveText(manifest?.targetPath)}`, 52), + ]]), + "", + "Recent samples:", + webObserveTable(["SEQ", "TS", "PATH", "ROUTE_SESSION", "ACTIVE_SESSION", "MSG", "TRACE"], samples.length > 0 ? samples.map((sample) => [ + webObserveText(sample.seq), + webObserveShort(webObserveText(sample.ts), 24), + webObserveShort(webObserveText(sample.path), 24), + webObserveShort(webObserveText(sample.routeSessionId), 20), + webObserveShort(webObserveText(sample.activeSessionId), 20), + webObserveText(sample.messageCount), + webObserveText(sample.traceRowCount), + ]) : [["-", "-", "-", "-", "-", "-", "-"]]), + "", + "Recent control:", + webObserveTable(["SEQ", "TS", "PHASE", "TYPE", "COMMAND"], controls.length > 0 ? controls.map((control) => [ + webObserveText(control.seq), + webObserveShort(webObserveText(control.ts), 24), + webObserveText(control.phase), + webObserveText(control.type), + webObserveShort(webObserveText(control.commandId), 28), + ]) : [["-", "-", "-", "-", "-"]]), + "", + "Network alerts:", + webObserveTable(["TS", "METHOD", "STATUS", "TYPE", "URL_OR_FAILURE"], network.length > 0 ? network.map((item) => [ + webObserveShort(webObserveText(item.ts), 24), + webObserveText(item.method), + webObserveText(item.status), + webObserveText(item.type), + webObserveShort(webObserveText(item.failure ?? item.url), 80), + ]) : [["-", "-", "-", "-", "-"]]), + "", + "Next:", + ]; + for (const key of ["status", "command", "stop", "analyze"] as const) { + const command = webObserveText(next?.[key]); + if (command !== "-") lines.push(` ${command}`); + } + lines.push("", "Disclosure:"); + lines.push(" default view is a bounded table; use observe collect/analyze for artifact-level drill-down."); + return lines.join("\n"); +} + +function webObserveTable(headers: string[], rows: string[][]): string { + const normalizedRows = rows.map((row) => headers.map((_, index) => row[index] ?? "")); + const widths = headers.map((header, index) => Math.max(header.length, ...normalizedRows.map((row) => row[index].length))); + return [ + headers.map((header, index) => header.padEnd(widths[index])).join(" ").trimEnd(), + ...normalizedRows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd()), + ].join("\n"); +} + +function webObserveText(value: unknown): string { + if (value === null || value === undefined || value === "") return "-"; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + return JSON.stringify(value); +} + +function webObserveShort(value: string, maxLength: number): string { + if (value.length <= maxLength) return value; + if (maxLength <= 1) return value.slice(0, maxLength); + return `${value.slice(0, maxLength - 1)}~`; +} + function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, stopCommand: boolean): Record { const type = options.commandType ?? (stopCommand ? "stop" : null); if (type === null) throw new Error("web-probe observe command requires --type"); diff --git a/scripts/src/hwlab-node.ts b/scripts/src/hwlab-node.ts index 62668906..237d3087 100644 --- a/scripts/src/hwlab-node.ts +++ b/scripts/src/hwlab-node.ts @@ -1,6 +1,7 @@ // SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0. // Responsibility: Thin HWLAB node/lane CLI entrypoint. Domain implementation lives in focused modules. import type { Config } from "./config"; +import type { RenderedCliResult } from "./output"; import { hwlabNodeHelp as hwlabNodeHelpImpl, hwlabNodeObservabilityHelp as hwlabNodeObservabilityHelpImpl, @@ -9,7 +10,7 @@ import { runHwlabNodeCommand as runHwlabNodeCommandImpl, } from "./hwlab-node-impl"; -export async function runHwlabNodeCommand(config: Config, args: string[]): Promise> { +export async function runHwlabNodeCommand(config: Config, args: string[]): Promise | RenderedCliResult> { return runHwlabNodeCommandImpl(config, args); } diff --git a/scripts/src/platform-infra-observability.ts b/scripts/src/platform-infra-observability.ts index f887b801..8f93c45c 100644 --- a/scripts/src/platform-infra-observability.ts +++ b/scripts/src/platform-infra-observability.ts @@ -791,7 +791,10 @@ function renderTraceTable(input: { query: Record; result: Record; }): RenderedCliResult { - const spanRows = uniqueSpanRecords([...asArray(input.result.errorSpans), ...asArray(input.result.spans)]) + const spanSource = input.result.grep === null || input.result.grep === undefined + ? [...asArray(input.result.errorSpans), ...asArray(input.result.spans)] + : asArray(input.result.spans); + const spanRows = uniqueSpanRecords(spanSource) .slice(0, 10) .map((span) => { return [