fix: restore web sentinel recovery diagnostics (#980)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p8-web-probe-sentinel-recovery.
|
||||
// Responsibility: Redacted web-probe summaries and issue-ready compaction helpers.
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
@@ -392,7 +393,8 @@ function webProbeRunFailureKind(degradedReason: string | null, promptValidation:
|
||||
if (degradedReason === "agent-terminal-timeout") return "agent-runtime-timeout";
|
||||
if (/trace-fetch|api|fetch|http|network/iu.test(degradedReason)) return "network-or-api-fetch-bug";
|
||||
if (/auth|login|credential/iu.test(degradedReason)) return "target-auth-bug";
|
||||
if (/browser|timeout|playwright|chromium/iu.test(degradedReason)) return "browser-environment-bug";
|
||||
if (/browser-timeout|selector-timeout|readiness|session|composer|navigation|load-jitter|timeout/iu.test(degradedReason)) return "target-page-readiness-timeout";
|
||||
if (/browser|playwright|chromium/iu.test(degradedReason)) return "browser-environment-bug";
|
||||
const failures = Array.isArray(promptValidation.failures) ? promptValidation.failures.join(" ") : "";
|
||||
if (/final-response|agent-message|markdown|completed/iu.test(failures)) return "unmet-expectation";
|
||||
return "user-facing-web-bug";
|
||||
@@ -424,6 +426,7 @@ function webProbeRunNextAction(
|
||||
if (failureKind === "network-or-api-fetch-bug") return "Inspect trace/session API fetch fields in reportPath and retry after checking target API availability.";
|
||||
if (failureKind === "agent-runtime-timeout") return "Inspect summary.traceId/sessionId and the corresponding Code Agent/AgentRun runtime; the browser probe submitted the prompt and observed trace events, but no terminal agent response arrived before the wait boundary.";
|
||||
if (failureKind === "target-auth-bug") return "Inspect credential sourceRef/fingerprint and /auth/login status; do not print secrets.";
|
||||
if (failureKind === "target-page-readiness-timeout") return "Inspect finalUrl, readiness/session-create state, Workbench loading indicators, network request failures, and session API evidence in reportPath before changing browser launcher settings.";
|
||||
if (failureKind === "browser-environment-bug") return "Inspect browser launcher and Playwright availability on the target workspace.";
|
||||
if (promptSubmitted && !traceRequested) return "Rerun with trace sampling if trace evidence is required.";
|
||||
return degradedReason === null ? null : "Inspect reportPath for full redacted details, then rerun the same node/lane entry.";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p8-web-probe-sentinel-recovery.
|
||||
// Responsibility: YAML-first CI/CD, image, GitOps and Argo command plan for the web-probe sentinel.
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
@@ -1353,10 +1354,28 @@ function sentinelPayloadFromLogs(logsTail: string): Record<string, unknown> {
|
||||
return {};
|
||||
}
|
||||
|
||||
function sentinelElapsedWarnings(value: unknown): string[] {
|
||||
function sentinelElapsedWarnings(value: unknown, subject = "sentinel confirmed operation"): string[] {
|
||||
const elapsedMs = typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
if (elapsedMs === null || elapsedMs <= 120_000) return [];
|
||||
return [`sentinel confirmed operation exceeded 120s (${Math.round(elapsedMs / 1000)}s); investigate env-reuse/git mirror/source build path before treating this as normal.`];
|
||||
return [`${subject} exceeded 120s (${Math.round(elapsedMs / 1000)}s); treat this as a severe timeout and investigate env-reuse/git mirror/source build path plus the current wait stage before retrying.`];
|
||||
}
|
||||
|
||||
function mergeWarnings(...items: readonly (readonly unknown[] | unknown)[]): string[] {
|
||||
const warnings: string[] = [];
|
||||
for (const item of items) {
|
||||
const values = Array.isArray(item) ? item : [item];
|
||||
for (const value of values) {
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
const warning = text(value).trim();
|
||||
if (warning.length > 0 && warning !== "-" && !warnings.includes(warning)) warnings.push(warning);
|
||||
}
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function withWarnings(payload: Record<string, unknown>, warnings: readonly unknown[]): Record<string, unknown> {
|
||||
const merged = mergeWarnings(payload.warnings, warnings);
|
||||
return merged.length === 0 ? payload : { ...payload, warnings: merged, valuesRedacted: true };
|
||||
}
|
||||
|
||||
function sentinelProgressEvent(event: string, payload: Record<string, unknown>): void {
|
||||
@@ -1469,6 +1488,7 @@ function runSentinelMaintenance(state: SentinelCicdState, options: Extract<WebPr
|
||||
|
||||
function runSentinelValidate(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "validate" }>): RenderedCliResult {
|
||||
const command = "web-probe sentinel validate";
|
||||
const startedAt = Date.now();
|
||||
const initialHealth = callSentinelService(state, "GET", "/api/health", null, options.timeoutSeconds);
|
||||
let quickVerify: Record<string, unknown> | null = null;
|
||||
if (options.quickVerify) {
|
||||
@@ -1508,6 +1528,9 @@ function runSentinelValidate(state: SentinelCicdState, options: Extract<WebProbe
|
||||
const report = callSentinelService(state, "GET", "/api/report?view=summary", null, options.timeoutSeconds);
|
||||
const publicExposure = probeSentinelPublicExposure(state, options.timeoutSeconds);
|
||||
const publicDashboard = probeSentinelPublicDashboard(state, options.timeoutSeconds);
|
||||
if (quickVerify !== null) {
|
||||
quickVerify = withWarnings(quickVerify, sentinelElapsedWarnings(Date.now() - startedAt, "sentinel validate quick verify confirm-wait"));
|
||||
}
|
||||
const ok = health.ok
|
||||
&& record(health.bodyJson).ok === true
|
||||
&& metrics.ok
|
||||
@@ -1572,6 +1595,9 @@ function renderAsyncP5Job(state: SentinelCicdState, subcommand: readonly string[
|
||||
}
|
||||
|
||||
function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeoutSeconds: number): Record<string, unknown> {
|
||||
const startedAt = Date.now();
|
||||
const elapsedMs = () => Date.now() - startedAt;
|
||||
const elapsedWarnings = () => sentinelElapsedWarnings(elapsedMs(), "quick verify confirm-wait");
|
||||
const scenarioId = stringAt(state.cicd, "targetValidation.scenarioId");
|
||||
const maxSeconds = numberAt(state.cicd, "targetValidation.maxSeconds");
|
||||
const scenario = findScenario(state, scenarioId);
|
||||
@@ -1595,6 +1621,7 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
steps.push({ phase: "observe-start", ok: started.ok, result: started.result });
|
||||
const observerId = observerIdFromText(String(record(started.result).stdoutPreview ?? ""));
|
||||
if (!started.ok || observerId === null) {
|
||||
const findings = quickVerifyControlFindings("observe-start-failed", 0, null, null);
|
||||
return recordQuickVerify(state, {
|
||||
ok: false,
|
||||
runId,
|
||||
@@ -1602,8 +1629,12 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
reason,
|
||||
status: "blocked",
|
||||
observerId,
|
||||
elapsedMs: elapsedMs(),
|
||||
steps,
|
||||
failure: "observe-start-failed",
|
||||
findingCount: findings.length,
|
||||
findings,
|
||||
warnings: elapsedWarnings(),
|
||||
valuesRedacted: true,
|
||||
});
|
||||
}
|
||||
@@ -1621,7 +1652,8 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
promptIndex,
|
||||
steps,
|
||||
failure: "quick-verify-timeout-over-120s",
|
||||
warnings: ["quick verify exceeded the configured 120s targetValidation budget; investigate env-reuse/git mirror/source build path before retrying."],
|
||||
elapsedMs: elapsedMs(),
|
||||
warnings: mergeWarnings("quick verify exceeded the configured 120s targetValidation budget; investigate env-reuse/git mirror/source build path before retrying.", elapsedWarnings()),
|
||||
promptSource: prompts.summary,
|
||||
}));
|
||||
}
|
||||
@@ -1642,6 +1674,8 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
promptIndex,
|
||||
steps,
|
||||
failure: `observe-command-${type}-failed`,
|
||||
elapsedMs: elapsedMs(),
|
||||
warnings: elapsedWarnings(),
|
||||
promptSource: prompts.summary,
|
||||
}));
|
||||
}
|
||||
@@ -1658,7 +1692,8 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
steps,
|
||||
failure: text(waitResult.failure ?? "observe-turn-terminal-wait-failed"),
|
||||
promptSource: prompts.summary,
|
||||
warnings: Array.isArray(waitResult.warnings) ? waitResult.warnings : [],
|
||||
elapsedMs: elapsedMs(),
|
||||
warnings: mergeWarnings(Array.isArray(waitResult.warnings) ? waitResult.warnings : [], elapsedWarnings()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1670,7 +1705,11 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
const artifactSummary = indexEntry === null ? { ok: false, reason: "observe-index-entry-missing", observerId, valuesRedacted: true } : readAnalysisSummaryFromWorkspace(state, indexEntry.stateDir, remainingSeconds(deadline, 30));
|
||||
const turnSummary = collectObserveView(state, observerId, "turn-summary", null, remainingSeconds(deadline, 30));
|
||||
const traceFrame = collectObserveView(state, observerId, "trace-frame", promptIndex > 0 ? promptIndex : null, remainingSeconds(deadline, 30));
|
||||
const ok = analysis.ok && record(artifactSummary).ok === true;
|
||||
const controlFindings = quickVerifyControlFindings(null, promptIndex, turnSummary, traceFrame);
|
||||
const artifactSummaryRecord = record(artifactSummary);
|
||||
const artifactFindings = Array.isArray(artifactSummaryRecord.findings) ? artifactSummaryRecord.findings.map(record) : [];
|
||||
const findings = mergeFindingRecords(artifactFindings, controlFindings);
|
||||
const ok = analysis.ok && record(artifactSummary).ok === true && controlFindings.length === 0;
|
||||
return recordQuickVerify(state, {
|
||||
ok,
|
||||
runId,
|
||||
@@ -1678,10 +1717,12 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
reason,
|
||||
status: ok ? "analyzed" : "blocked",
|
||||
observerId,
|
||||
elapsedMs: elapsedMs(),
|
||||
stateDir: indexEntry?.stateDir ?? null,
|
||||
reportJsonSha256: stringAtNullable(artifactSummary, "reportJsonSha256"),
|
||||
findingCount: numberAtNullable(artifactSummary, "findingCount") ?? 0,
|
||||
findingCount: findings.length,
|
||||
artifactCount: numberAtNullable(artifactSummary, "artifactCount") ?? 0,
|
||||
failure: controlFindings.length > 0 ? "quick-verify-no-business-turn" : null,
|
||||
promptSource: prompts.summary,
|
||||
steps,
|
||||
analysis: artifactSummary,
|
||||
@@ -1690,9 +1731,10 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
"turn-summary": { renderedText: typeof turnSummary.renderedText === "string" ? turnSummary.renderedText : null, ok: turnSummary.ok },
|
||||
"trace-frame": { renderedText: typeof traceFrame.renderedText === "string" ? traceFrame.renderedText : null, ok: traceFrame.ok },
|
||||
},
|
||||
findings: Array.isArray(record(artifactSummary).findings) ? record(artifactSummary).findings : [],
|
||||
findings,
|
||||
screenshot: record(artifactSummary).screenshot,
|
||||
publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"),
|
||||
warnings: elapsedWarnings(),
|
||||
valuesRedacted: true,
|
||||
});
|
||||
}
|
||||
@@ -1706,6 +1748,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
|
||||
readonly steps: readonly Record<string, unknown>[];
|
||||
readonly failure: string;
|
||||
readonly promptSource?: Record<string, unknown>;
|
||||
readonly elapsedMs?: number;
|
||||
readonly warnings?: readonly unknown[];
|
||||
}): Record<string, unknown> {
|
||||
const cleanupSteps: Record<string, unknown>[] = [];
|
||||
@@ -1741,6 +1784,10 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
|
||||
: readAnalysisSummaryFromWorkspace(state, indexEntry.stateDir, 30);
|
||||
const turnSummary = collectObserveView(state, input.observerId, "turn-summary", null, 30);
|
||||
const traceFrame = collectObserveView(state, input.observerId, "trace-frame", input.promptIndex > 0 ? input.promptIndex : null, 30);
|
||||
const controlFindings = quickVerifyControlFindings(input.failure, input.promptIndex, turnSummary, traceFrame);
|
||||
const artifactSummaryRecord = record(artifactSummary);
|
||||
const artifactFindings = Array.isArray(artifactSummaryRecord.findings) ? artifactSummaryRecord.findings.map(record) : [];
|
||||
const findings = mergeFindingRecords(artifactFindings, controlFindings);
|
||||
return {
|
||||
ok: false,
|
||||
runId: input.runId,
|
||||
@@ -1748,9 +1795,10 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
|
||||
reason: input.reason,
|
||||
status: "blocked",
|
||||
observerId: input.observerId,
|
||||
elapsedMs: input.elapsedMs ?? null,
|
||||
stateDir: indexEntry?.stateDir ?? null,
|
||||
reportJsonSha256: stringAtNullable(artifactSummary, "reportJsonSha256"),
|
||||
findingCount: numberAtNullable(artifactSummary, "findingCount") ?? 0,
|
||||
findingCount: findings.length,
|
||||
artifactCount: numberAtNullable(artifactSummary, "artifactCount") ?? 0,
|
||||
failure: input.failure,
|
||||
promptSource: input.promptSource,
|
||||
@@ -1761,10 +1809,10 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
|
||||
"turn-summary": { renderedText: typeof turnSummary.renderedText === "string" ? turnSummary.renderedText : null, ok: turnSummary.ok },
|
||||
"trace-frame": { renderedText: typeof traceFrame.renderedText === "string" ? traceFrame.renderedText : null, ok: traceFrame.ok },
|
||||
},
|
||||
findings: Array.isArray(record(artifactSummary).findings) ? record(artifactSummary).findings : [],
|
||||
findings,
|
||||
screenshot: record(artifactSummary).screenshot,
|
||||
publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"),
|
||||
warnings: Array.isArray(input.warnings) ? input.warnings.map(text) : [],
|
||||
warnings: mergeWarnings(Array.isArray(input.warnings) ? input.warnings : [], sentinelElapsedWarnings(input.elapsedMs ?? null, "quick verify confirm-wait")),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
@@ -1782,6 +1830,9 @@ function recordQuickVerify(state: SentinelCicdState, payload: Record<string, unk
|
||||
summary: {
|
||||
reason: payload.reason,
|
||||
status: payload.status,
|
||||
elapsedMs: payload.elapsedMs,
|
||||
failure: payload.failure,
|
||||
warnings: Array.isArray(payload.warnings) ? payload.warnings : [],
|
||||
analysis: payload.analysis,
|
||||
promptSource: payload.promptSource,
|
||||
steps: payload.steps,
|
||||
@@ -2464,6 +2515,40 @@ function displayPath(pathValue: string): string {
|
||||
return pathValue;
|
||||
}
|
||||
|
||||
function mergeFindingRecords(primary: readonly Record<string, unknown>[], extra: readonly Record<string, unknown>[]): Record<string, unknown>[] {
|
||||
const merged: Record<string, unknown>[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of [...primary, ...extra]) {
|
||||
const id = stringAtNullable(item, "id") ?? stringAtNullable(item, "kind") ?? stringAtNullable(item, "code") ?? stringAtNullable(item, "finding_id") ?? "finding";
|
||||
const severity = stringAtNullable(item, "severity") ?? stringAtNullable(item, "level") ?? "unknown";
|
||||
const key = `${id}\0${severity}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
merged.push(item);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function quickVerifyControlFindings(failure: string | null, promptIndex: number, turnSummary: Record<string, unknown> | null, traceFrame: Record<string, unknown> | null): Record<string, unknown>[] {
|
||||
const rendered = [
|
||||
typeof turnSummary?.renderedText === "string" ? turnSummary.renderedText : "",
|
||||
typeof traceFrame?.renderedText === "string" ? traceFrame.renderedText : "",
|
||||
].join("\n");
|
||||
const noPrompt = promptIndex <= 0 || /无\s*sendPrompt|no\s+sendPrompt/iu.test(rendered);
|
||||
const noTrace = /无\s*trace\s*rows|no\s+trace\s+rows|traceId=-|routeSession=-|activeSession=-/iu.test(rendered);
|
||||
const emptyFinal = /Final Response[\s\S]*\(空内容\)/iu.test(rendered);
|
||||
if (!noPrompt && !noTrace && !emptyFinal && failure !== "observe-start-failed") return [];
|
||||
return [{
|
||||
id: "quick-verify-no-business-turn",
|
||||
severity: "red",
|
||||
count: 1,
|
||||
summary: "quick verify did not reach a durable business turn/session/trace rows/final response; public dashboard health cannot be treated as HWLAB recovery.",
|
||||
failure: failure ?? null,
|
||||
promptIndex,
|
||||
valuesRedacted: true,
|
||||
}];
|
||||
}
|
||||
|
||||
function compactCommandWithTail(result: CommandResult): CompactCommandResult & { stdoutTail: string; stderrTail: string } {
|
||||
return {
|
||||
...compactCommand(result),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p7-web-probe-sentinel-dashboard.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p8-web-probe-sentinel-recovery.
|
||||
// Responsibility: Static dashboard shell and asset serving for the web-probe sentinel frontend.
|
||||
import { readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
@@ -11,7 +11,7 @@ interface DashboardShellConfig {
|
||||
}
|
||||
|
||||
const DASHBOARD_ASSET_ROOT = "scripts/assets/web-probe-sentinel-dashboard";
|
||||
const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p7-web-probe-sentinel-dashboard";
|
||||
const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p8-web-probe-sentinel-recovery";
|
||||
|
||||
export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig): string {
|
||||
const publicOrigin = stringOrNull(config.publicExposure.publicBaseUrl) ?? "";
|
||||
@@ -20,7 +20,7 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HWLAB Web Probe Sentinel</title>
|
||||
<title>HWLAB Web哨兵</title>
|
||||
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -33,58 +33,58 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
data-config-ready="${config.plan.ok ? "true" : "false"}"
|
||||
data-contract-version="${DASHBOARD_CONTRACT_VERSION}"
|
||||
>
|
||||
<section class="sentinel-topbar" aria-label="Web probe sentinel overview">
|
||||
<section class="sentinel-topbar" aria-label="Web哨兵概览">
|
||||
<div class="sentinel-title">
|
||||
<div class="sentinel-mark" aria-hidden="true"></div>
|
||||
<div>
|
||||
<h1>HWLAB Web Probe Sentinel</h1>
|
||||
<h1>HWLAB Web哨兵</h1>
|
||||
<p id="sentinel-subtitle">${escapeHtml(config.node)} / ${escapeHtml(config.lane)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sentinel-toolbar" aria-label="Dashboard controls">
|
||||
<span id="status-pill" class="status-pill status-idle">idle</span>
|
||||
<div class="sentinel-toolbar" aria-label="监控面板控制">
|
||||
<span id="status-pill" class="status-pill status-idle">空闲</span>
|
||||
<label class="refresh-toggle">
|
||||
<input id="auto-refresh-enabled" type="checkbox">
|
||||
<span>Auto refresh</span>
|
||||
<span>自动刷新</span>
|
||||
</label>
|
||||
<select id="auto-refresh-interval" aria-label="Auto refresh interval">
|
||||
<select id="auto-refresh-interval" aria-label="自动刷新间隔">
|
||||
<option value="5">5s</option>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
</select>
|
||||
<button id="manual-refresh" class="icon-button" type="button" title="Refresh" aria-label="Refresh">
|
||||
<span aria-hidden="true">Refresh</span>
|
||||
<button id="manual-refresh" class="icon-button" type="button" title="刷新" aria-label="刷新">
|
||||
<span aria-hidden="true">刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="loading-banner" class="banner banner-muted">Loading</section>
|
||||
<section id="loading-banner" class="banner banner-muted">加载中</section>
|
||||
<section id="error-banner" class="banner banner-danger" hidden></section>
|
||||
|
||||
<section class="metric-grid" aria-label="Sentinel status metrics">
|
||||
<section class="metric-grid" aria-label="哨兵状态指标">
|
||||
<article class="metric-card">
|
||||
<span class="metric-label">Overall</span>
|
||||
<span class="metric-label">总体状态</span>
|
||||
<strong id="metric-overall">-</strong>
|
||||
<small id="metric-origin">-</small>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span class="metric-label">Latest run</span>
|
||||
<span class="metric-label">最近运行</span>
|
||||
<strong id="metric-latest-run">-</strong>
|
||||
<small id="metric-latest-age">-</small>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span class="metric-label">Findings</span>
|
||||
<span class="metric-label">发现项</span>
|
||||
<strong id="metric-findings">0</strong>
|
||||
<small id="metric-findings-note">-</small>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span class="metric-label">Scheduler</span>
|
||||
<span class="metric-label">调度器</span>
|
||||
<strong id="metric-scheduler">-</strong>
|
||||
<small id="metric-budget">-</small>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="overview-checks" aria-label="Sentinel health checks">
|
||||
<section class="overview-checks" aria-label="哨兵健康检查">
|
||||
<span id="check-config" class="check-chip">config -</span>
|
||||
<span id="check-pvc" class="check-chip">pvc -</span>
|
||||
<span id="check-analyzer" class="check-chip">analyzer -</span>
|
||||
@@ -94,7 +94,7 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
|
||||
<section class="panel timeline-panel" aria-labelledby="timeline-heading">
|
||||
<div class="panel-header">
|
||||
<h2 id="timeline-heading">Run Timeline</h2>
|
||||
<h2 id="timeline-heading">运行时间线</h2>
|
||||
<span id="timeline-count" class="panel-subtitle">-</span>
|
||||
</div>
|
||||
<div id="run-timeline" class="run-timeline"></div>
|
||||
@@ -103,34 +103,34 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
<section class="dashboard-grid">
|
||||
<section class="panel panel-wide" aria-labelledby="runs-heading">
|
||||
<div class="panel-header">
|
||||
<h2 id="runs-heading">Runs</h2>
|
||||
<h2 id="runs-heading">运行历史</h2>
|
||||
<span id="runs-count" class="panel-subtitle">-</span>
|
||||
</div>
|
||||
<form id="runs-filter" class="runs-filter">
|
||||
<label>
|
||||
<span>Status</span>
|
||||
<span>状态</span>
|
||||
<select id="filter-status" name="status">
|
||||
<option value="">Any</option>
|
||||
<option value="planned">planned</option>
|
||||
<option value="running">running</option>
|
||||
<option value="analyzed">analyzed</option>
|
||||
<option value="blocked">blocked</option>
|
||||
<option value="interrupted">interrupted</option>
|
||||
<option value="">全部</option>
|
||||
<option value="planned">已计划</option>
|
||||
<option value="running">运行中</option>
|
||||
<option value="analyzed">已分析</option>
|
||||
<option value="blocked">阻塞</option>
|
||||
<option value="interrupted">已中断</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Severity</span>
|
||||
<span>严重级别</span>
|
||||
<select id="filter-severity" name="severity">
|
||||
<option value="">Any</option>
|
||||
<option value="red">red</option>
|
||||
<option value="warning">warning</option>
|
||||
<option value="info">info</option>
|
||||
<option value="">全部</option>
|
||||
<option value="red">红色</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="info">信息</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Time</span>
|
||||
<span>时间</span>
|
||||
<select id="filter-window" name="window">
|
||||
<option value="">All</option>
|
||||
<option value="">全部</option>
|
||||
<option value="1h">1h</option>
|
||||
<option value="6h">6h</option>
|
||||
<option value="24h">24h</option>
|
||||
@@ -138,29 +138,29 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Sort</span>
|
||||
<span>排序</span>
|
||||
<select id="filter-sort" name="sort">
|
||||
<option value="updated">updated</option>
|
||||
<option value="created">created</option>
|
||||
<option value="findings">findings</option>
|
||||
<option value="severity">severity</option>
|
||||
<option value="updated">按更新时间</option>
|
||||
<option value="created">按创建时间</option>
|
||||
<option value="findings">按发现数量</option>
|
||||
<option value="severity">按严重级别</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="filter-search">
|
||||
<span>Search</span>
|
||||
<input id="filter-search" name="search" type="search" placeholder="run, observer, report">
|
||||
<span>搜索</span>
|
||||
<input id="filter-search" name="search" type="search" placeholder="run、observer、report">
|
||||
</label>
|
||||
<button id="clear-filters" class="icon-button" type="button">Clear</button>
|
||||
<button id="clear-filters" class="icon-button" type="button">清除</button>
|
||||
</form>
|
||||
<div class="table-frame">
|
||||
<table class="runs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Run</th>
|
||||
<th>Status</th>
|
||||
<th>Scenario</th>
|
||||
<th>Findings</th>
|
||||
<th>Updated</th>
|
||||
<th>运行</th>
|
||||
<th>状态</th>
|
||||
<th>场景</th>
|
||||
<th>发现项</th>
|
||||
<th>更新时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="runs-body"></tbody>
|
||||
@@ -170,38 +170,38 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
|
||||
<section class="panel" aria-labelledby="findings-heading">
|
||||
<div class="panel-header">
|
||||
<h2 id="findings-heading">Findings</h2>
|
||||
<h2 id="findings-heading">发现分析</h2>
|
||||
<span id="findings-count" class="panel-subtitle">-</span>
|
||||
</div>
|
||||
<form id="findings-filter" class="findings-filter">
|
||||
<label>
|
||||
<span>Severity</span>
|
||||
<span>严重级别</span>
|
||||
<select id="finding-filter-severity" name="fseverity">
|
||||
<option value="">Any</option>
|
||||
<option value="red">red</option>
|
||||
<option value="warning">warning</option>
|
||||
<option value="info">info</option>
|
||||
<option value="">全部</option>
|
||||
<option value="red">红色</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="info">信息</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Window</span>
|
||||
<span>窗口</span>
|
||||
<select id="finding-filter-window" name="fwindow">
|
||||
<option value="24h">24h</option>
|
||||
<option value="1h">1h</option>
|
||||
<option value="6h">6h</option>
|
||||
<option value="7d">7d</option>
|
||||
<option value="">All</option>
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Code</span>
|
||||
<span>代码</span>
|
||||
<input id="finding-filter-code" name="fcode" type="search" placeholder="finding code">
|
||||
</label>
|
||||
<label>
|
||||
<span>Scenario</span>
|
||||
<span>场景</span>
|
||||
<input id="finding-filter-scenario" name="fscenario" type="search" placeholder="scenario">
|
||||
</label>
|
||||
<button id="finding-clear-filters" class="icon-button" type="button">Clear</button>
|
||||
<button id="finding-clear-filters" class="icon-button" type="button">清除</button>
|
||||
</form>
|
||||
<div id="finding-aggregation" class="finding-aggregation"></div>
|
||||
<div id="findings-list" class="finding-list"></div>
|
||||
@@ -210,8 +210,8 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
|
||||
<section class="panel detail-panel" aria-labelledby="detail-heading" role="dialog" aria-modal="false">
|
||||
<div class="panel-header">
|
||||
<h2 id="detail-heading">Run Detail</h2>
|
||||
<span id="detail-subtitle" class="panel-subtitle">No run selected</span>
|
||||
<h2 id="detail-heading">运行详情</h2>
|
||||
<span id="detail-subtitle" class="panel-subtitle">未选择运行</span>
|
||||
</div>
|
||||
<div id="detail-content" class="detail-content"></div>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p7-web-probe-sentinel-dashboard.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p8-web-probe-sentinel-recovery.
|
||||
// Responsibility: Persistent HTTP wrapper service for web-probe observe scheduling, index, health, metrics, maintenance, and dashboard.
|
||||
import { Buffer } from "node:buffer";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
@@ -11,7 +12,7 @@ import { renderWebProbeSentinelDashboardHtml, webProbeSentinelDashboardAssetResp
|
||||
import { webProbeSentinelConfigPlan, type WebProbeSentinelConfigPlan } from "./hwlab-node-web-sentinel-config";
|
||||
import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
|
||||
|
||||
const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p7-web-probe-sentinel-dashboard";
|
||||
const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p8-web-probe-sentinel-recovery";
|
||||
const DASHBOARD_MAX_TEXT_BYTES = 16_000;
|
||||
|
||||
export interface WebProbeSentinelServiceConfig {
|
||||
|
||||
Reference in New Issue
Block a user