fix: make sentinel trend chart show duration

This commit is contained in:
Codex
2026-07-01 01:23:38 +00:00
parent b41a261ebb
commit 473b4f2300
6 changed files with 487 additions and 208 deletions
@@ -343,30 +343,12 @@ select {
font-size: 13px;
}
.trend-red {
fill: none;
stroke: var(--red);
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
.trend-warning {
fill: none;
stroke: var(--amber);
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
.trend-total {
.trend-duration {
fill: none;
stroke: var(--blue);
stroke-width: 2;
stroke-dasharray: 5 6;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.75;
}
.trend-grid-line {
@@ -374,12 +356,10 @@ select {
stroke-width: 1;
}
.trend-dot-red {
fill: var(--red);
}
.trend-dot-warning {
fill: var(--amber);
.trend-dot-duration {
fill: var(--blue);
stroke: #ffffff;
stroke-width: 1.5;
}
.trend-dot-hit {
@@ -450,6 +430,10 @@ select {
background: var(--red);
}
.legend-swatch.duration {
background: var(--blue);
}
.legend-swatch.warning {
background: var(--amber);
}
@@ -4,123 +4,6 @@ import { createApp, computed, onMounted, ref, watch } from "./vendor/vue.esm-bro
const bootstrap = readBootstrap();
const internalTextPattern = /水合|投影|Trace|trace|Shell|API|DOM|Console|console|Runner|runner|JSONL|steer|facts|分页|HTTP|http|requestfailed|pageerror|Final Response|Code Agent|web-probe|observe|analyzer|终态/u;
const checkDisplayCatalog = {
"code-agent-card-elapsed-missing": {
code: "CHECK-101",
title: "耗时未显示",
summary: "运行卡片没有显示总耗时,影响判断等待时间。",
},
"code-agent-card-running-recent-update-missing": {
code: "CHECK-102",
title: "最近更新时间未显示",
summary: "运行中的卡片没有显示最近更新时间,影响判断是否仍在推进。",
},
"turn-timing-total-elapsed-decrease": {
code: "CHECK-103",
title: "耗时显示回退",
summary: "相邻采样中的总耗时出现回退,作为非阻塞计时报警展示。",
},
"page-loading-concurrent": {
code: "CHECK-201",
title: "多个加载状态同时出现",
summary: "同一采样点看到多个加载提示,用户可能难以判断当前页面是否已就绪。",
},
"trace-events-page-read-requestfailed-root-cause": {
code: "CHECK-301",
title: "运行事件读取失败",
summary: "运行事件页面读取失败,需要结合服务端链路确认具体失败位置。",
},
"runtime-requestfailed": {
code: "CHECK-302",
title: "页面资源加载失败",
summary: "巡检期间捕获到页面资源加载失败,可能影响用户看到的运行状态。",
},
"runtime-console-alerts": {
code: "CHECK-303",
title: "页面脚本报警",
summary: "巡检期间捕获到页面脚本报警,需要评估是否影响用户操作。",
},
"workbench-app-shell-transient-not-ready": {
code: "CHECK-304",
title: "工作台短暂未就绪",
summary: "工作台短时间内未完成主界面就绪,作为启动过程上下文保留。",
},
"cross-page-projection-divergence": {
code: "CHECK-401",
title: "多页面状态不一致",
summary: "同一会话在两个页面中显示的状态持续不一致,可能影响用户判断。",
},
"cross-page-projection-transient-divergence": {
code: "CHECK-402",
title: "多页面短暂不一致",
summary: "页面切换附近短暂出现显示差异,作为上下文保留。",
},
"cross-page-projection-controlled-navigation-hydration": {
code: "CHECK-403",
title: "跳转后页面短暂空白",
summary: "页面跳转后短暂空白,当前按非阻塞上下文展示。",
},
"cross-page-trace-visibility-divergence": {
code: "CHECK-404",
title: "运行记录显示数量不一致",
summary: "两个页面看到的运行记录数量不同,但未直接证明消息状态不一致。",
},
"page-performance-slow-same-origin-api": {
code: "CHECK-501",
title: "页面数据加载慢",
summary: "页面数据加载超过配置预算,用户可能感觉等待时间过长。",
},
"page-performance-long-lived-streams": {
code: "CHECK-502",
title: "长连接持续保持",
summary: "长时间连接单独统计,不直接作为页面加载慢处理。",
},
"natural-api-dom-lag-baseline": {
code: "CHECK-503",
title: "数据变化可关联",
summary: "已采集到数据响应和页面变化样本,可用于分析显示延迟。",
},
"natural-api-dom-lag-candidates": {
code: "CHECK-504",
title: "数据更新显示偏慢",
summary: "部分数据变化到页面显示的时间超过预算,作为非阻塞调查报警。",
},
"browser-console-or-page-errors": {
code: "CHECK-601",
title: "页面脚本错误",
summary: "巡检期间捕获到页面脚本错误,需要确认是否影响用户流程。",
},
"quick-verify-no-business-turn": {
code: "CHECK-701",
title: "业务轮次未完成",
summary: "巡检未确认到稳定的业务交互结果,不能作为恢复证据。",
},
"frontend-browser-memory-rss-red": {
code: "CHECK-801",
title: "页面内存超出预算",
summary: "页面有效内存超过配置红线,已按阻塞错误展示。",
},
"frontend-browser-memory-growth-red": {
code: "CHECK-802",
title: "页面内存持续增长",
summary: "页面有效内存在观察窗口内持续增长并超过配置预算,符合卡死风险特征。",
},
"frontend-browser-freeze-runner-blocker": {
code: "CHECK-803",
title: "浏览器冻结已阻塞",
summary: "浏览器冻结策略已触发,巡检中止页面并报红,不能用刷新或兜底掩盖。",
},
"frontend-playwright-responsiveness-red": {
code: "CHECK-804",
title: "页面响应探测超时",
summary: "页面响应探测超过配置预算,已按前端卡死处理并报红。",
},
"frontend-cdp-metrics-timeout-red": {
code: "CHECK-805",
title: "浏览器指标采集超时",
summary: "浏览器侧指标连续采集超时,说明页面运行时可能已经无响应。",
},
};
createApp({
setup() {
@@ -165,19 +48,18 @@ createApp({
const selectedRun = computed(() => runs.value.find((run) => run.id === selectedRunId.value) || latestRun.value);
const trendRows = computed(() => runs.value.slice().sort((a, b) => Date.parse(a.updatedAt || a.createdAt || "") - Date.parse(b.updatedAt || b.createdAt || "")).slice(-48));
const latestTrendRun = computed(() => trendRows.value.length > 0 ? trendRows.value[trendRows.value.length - 1] : latestRun.value);
const trendMax = computed(() => Math.max(1, ...trendRows.value.flatMap((run) => [trendErrorCount(run), trendWarningCount(run), trendTotalCount(run)])));
const trendDurationMax = computed(() => Math.max(0, ...trendRows.value.map((run) => trendDurationMinutes(run))));
const trendMax = computed(() => Math.max(1, trendDurationMax.value));
const trendPolylines = computed(() => ({
red: trendPolyline((run) => trendErrorCount(run)),
warning: trendPolyline((run) => trendWarningCount(run)),
total: trendPolyline((run) => trendTotalCount(run)),
duration: trendPolyline((run) => trendDurationMinutes(run)),
}));
const trendDots = computed(() => trendRows.value.map((run, index) => {
const red = trendErrorCount(run);
const warning = trendWarningCount(run);
const total = trendTotalCount(run);
const duration = trendDurationMinutes(run);
const x = trendX(index, trendRows.value.length);
const redY = trendY(red);
const warningY = trendY(warning);
const durationY = trendY(duration);
const rawTime = run.updatedAt || run.createdAt || "";
const durationText = runDurationText(run);
const configTimingText = runTimingConfigText(run);
@@ -185,10 +67,10 @@ createApp({
id: run.id || String(index),
runId: run.id || "",
x,
redY,
warningY,
durationY,
duration,
tooltipLeft: `${clamp((x / 720) * 100, 16, 84)}%`,
tooltipTop: `${clamp(((Math.min(redY, warningY) + 18) / 142) * 100, 24, 76)}%`,
tooltipTop: `${clamp(((durationY + 18) / 142) * 100, 24, 76)}%`,
red,
warning,
total,
@@ -363,6 +245,10 @@ createApp({
return runCheckAlertCount(run);
}
function trendDurationMinutes(run) {
return number(run?.runDurationMinutes ?? run?.durationMinutes ?? run?.timing?.durationMinutes);
}
function openCheckDetail(item) {
activeCheckItem.value = item || null;
}
@@ -459,6 +345,7 @@ createApp({
selectedRun,
trendRows,
latestTrendRun,
trendDurationMax,
trendPolylines,
trendDots,
timelineRuns,
@@ -492,12 +379,14 @@ createApp({
trendTotalCount,
trendErrorCount,
trendWarningCount,
trendDurationMinutes,
runDurationText,
runTimingConfigText,
severityClass,
formatDate,
formatAbsoluteDate,
formatDuration,
formatMinutes,
shortId,
rootCauseText,
findingTitle,
@@ -566,19 +455,17 @@ createApp({
<section class="trend-panel" aria-labelledby="trend-heading">
<div class="panel-header">
<div>
<h2 id="trend-heading">错误 / 告警监测项曲线</h2>
<p>按运行更新时间展示最近 {{ trendRows.length }} 次变化,点位为单次运行监测项行数</p>
<h2 id="trend-heading">运行耗时曲线</h2>
<p>按运行更新时间展示最近 {{ trendRows.length }} 次巡检耗时,单位为分钟</p>
</div>
<span class="pill" :class="cadence.stale ? 'warning' : 'healthy'">{{ cadence.stale ? "非阻塞报警" : "新鲜" }}</span>
</div>
<div class="trend-chart-wrap">
<svg class="trend-chart" viewBox="0 0 720 142" role="img" aria-label="错误和告警监测项数量趋势" data-monitor-trend-curve="true">
<svg class="trend-chart" viewBox="0 0 720 142" role="img" aria-label="运行耗时分钟趋势" data-monitor-trend-curve="true">
<line class="trend-grid-line" x1="24" x2="696" y1="24" y2="24"></line>
<line class="trend-grid-line" x1="24" x2="696" y1="75" y2="75"></line>
<line class="trend-grid-line" x1="24" x2="696" y1="126" y2="126"></line>
<polyline v-if="trendPolylines.total" class="trend-total" :points="trendPolylines.total"></polyline>
<polyline v-if="trendPolylines.warning" class="trend-warning" :points="trendPolylines.warning"></polyline>
<polyline v-if="trendPolylines.red" class="trend-red" :points="trendPolylines.red"></polyline>
<polyline v-if="trendPolylines.duration" class="trend-duration" :points="trendPolylines.duration"></polyline>
<g
v-for="dot in trendDots"
:key="dot.id"
@@ -591,11 +478,8 @@ createApp({
@mouseleave="hideTrendTooltip"
@focusout="hideTrendTooltip"
>
<circle class="trend-hit-area" :cx="dot.x" :cy="Math.min(dot.redY, dot.warningY)" r="12"></circle>
<circle class="trend-dot-warning" :cx="dot.x" :cy="dot.warningY" r="3">
<title>{{ dot.title }}</title>
</circle>
<circle class="trend-dot-red" :cx="dot.x" :cy="dot.redY" r="3">
<circle class="trend-hit-area" :cx="dot.x" :cy="dot.durationY" r="12"></circle>
<circle class="trend-dot-duration" :cx="dot.x" :cy="dot.durationY" r="4">
<title>{{ dot.title }}</title>
</circle>
</g>
@@ -617,9 +501,9 @@ createApp({
<div v-if="trendRows.length === 0" class="trend-empty">暂无运行数据</div>
</div>
<div class="trend-legend">
<span class="legend-item"><span class="legend-swatch red"></span>最新点错误 {{ trendErrorCount(latestTrendRun) }}</span>
<span class="legend-item"><span class="legend-swatch warning"></span>最新点告警 {{ trendWarningCount(latestTrendRun) }}</span>
<span class="legend-item"><span class="legend-swatch total"></span>最新点错误+告警合计 {{ trendTotalCount(latestTrendRun) }}</span>
<span class="legend-item"><span class="legend-swatch duration"></span>最新运行耗时 {{ runDurationText(latestTrendRun) }}</span>
<span class="legend-item">最近最高耗时 {{ formatMinutes(trendDurationMax) }}</span>
<span class="legend-item">最新错误 {{ trendErrorCount(latestTrendRun) }} / 告警 {{ trendWarningCount(latestTrendRun) }}</span>
<span class="legend-item">历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 告警 {{ warningCount({ severityCounts: severityTotals }) }}</span>
<span class="legend-item">{{ cadence.alert }}</span>
</div>
@@ -1127,7 +1011,7 @@ function clamp(value, min, max) {
function rootCauseText(item) {
const display = checkDisplay(item);
if (display.summary) return display.summary;
const safeSummary = safeUserText(item?.checkSummaryZh || item?.summary || item?.evidenceSummary);
const safeSummary = safeUserText(item?.checkSummaryZh || item?.check?.summaryZh);
if (safeSummary) return safeSummary;
if (item?.rootCause) return "已记录内部根因,见报告详情。";
return "尚未记录用户可见问题摘要。";
@@ -1201,7 +1085,8 @@ function checkTimeText(item) {
}
function checkActionText(item) {
return safeUserText(item?.nextAction || item?.action || item?.recommendation) || "查看详情后处理";
const display = checkDisplay(item);
return display.action || safeUserText(item?.checkActionZh || item?.check?.actionZh) || "查看详情后处理";
}
function checkDetailRows(item) {
@@ -1223,7 +1108,7 @@ function checkEvidenceRows(item) {
if (!item) return [{ key: "empty", label: "状态", value: "未选择监测项" }];
const evidence = item?.evidence && typeof item.evidence === "object" && !Array.isArray(item.evidence) ? item.evidence : {};
const rows = [
{ key: "summary", label: "证据摘要", value: safeUserText(item?.evidenceSummary || evidence.summary || item?.summary) || rootCauseText(item) },
{ key: "summary", label: "证据摘要", value: rootCauseText(item) || safeUserText(item?.evidenceSummary || evidence.summary) },
{ key: "sample", label: "样本序号", value: safeDetailValue(item?.sampleSeq ?? evidence.sampleSeq) },
{ key: "page", label: "页面", value: safeDetailValue(item?.pageRole || evidence.pageRole) },
{ key: "command", label: "命令编号", value: safeDetailValue(item?.commandId || evidence.commandId) },
@@ -1254,20 +1139,12 @@ function safeDetailValue(value) {
function checkDisplay(item) {
const rawCode = rawCheckCode(item);
const rawId = rawFindingId(item);
const registered = checkDisplayCatalog[rawId] || checkDisplayCatalog[rawCode];
const serviceCode = publicCheckCode(item?.checkCode || item?.check?.code);
if (registered) {
return {
...registered,
code: serviceCode || registered.code,
title: safeUserText(item?.checkTitleZh || item?.check?.titleZh) || registered.title,
summary: safeUserText(item?.checkSummaryZh || item?.summary || item?.evidenceSummary || item?.check?.summaryZh) || registered.summary,
};
}
return {
code: serviceCode || publicCheckCode(rawId || rawCode) || stableCheckCode(rawCode || rawId),
title: safeUserText(item?.checkTitleZh || item?.check?.titleZh) || "未登记监测项",
summary: safeUserText(item?.checkSummaryZh || item?.summary || item?.evidenceSummary) || "已记录监测项详情,见报告原文。",
summary: safeUserText(item?.checkSummaryZh || item?.check?.summaryZh) || "已记录监测项详情,见报告原文。",
action: safeUserText(item?.checkActionZh || item?.check?.actionZh) || "查看详情后处理",
};
}
+74 -1
View File
@@ -1,6 +1,8 @@
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
// Responsibility: Redacted YAML configRef graph for web-probe sentinel plan/status.
import { readFileSync } from "node:fs";
import { rootPath } from "./config";
import { readWebProbeSentinelConfigRef } from "./hwlab-node-web-sentinel-config-ref";
import { HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS, type HwlabRuntimeLaneSpec, type HwlabRuntimeWebProbeSentinelConfigRefKey } from "./hwlab-node-lanes";
import { effectiveWebProbeSentinelPublicExposure, resolveWebProbeSentinel, webProbeSentinelRegistryRows, type WebProbeSentinelRegistryRow } from "./hwlab-node-web-sentinel-resolver";
@@ -100,7 +102,7 @@ const REQUIRED_TARGET_SHAPES: Record<HwlabRuntimeWebProbeSentinelConfigRefKey, R
},
reportViews: {
kind: "object",
requiredPaths: ["defaultView", "views[0]", "pageSize", "maxPageSize", "rawAccess", "redaction.prompt", "redaction.secrets"],
requiredPaths: ["defaultView", "views[0]", "pageSize", "maxPageSize", "rawAccess", "checkCatalogRef", "redaction.prompt", "redaction.secrets"],
},
publicExposure: {
kind: "object",
@@ -320,6 +322,7 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
const runtime = recordTarget(byKey.get("runtime"));
const scenarios = scenarioTargets(byKey.get("scenarios"));
const promptSet = recordTarget(byKey.get("promptSet"));
const reportViews = recordTarget(byKey.get("reportViews"));
const cicd = recordTarget(byKey.get("cicd"));
const secrets = recordTarget(byKey.get("secrets"));
@@ -350,6 +353,8 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
conflicts.push(`${byKey.get("cicd")?.file}#${byKey.get("cicd")?.path}.targetValidation.scenarioId=${validationScenarioId} is not declared in scenarios`);
}
validateReportViewsCheckCatalog(spec, conflicts, byKey.get("reportViews"), reportViews);
const runtimeNamespace = runtime === null ? null : stringAt(runtime, "namespace");
const runtimeSecrets = secrets === null ? [] : arrayAt(secrets, "runtimeSecrets");
if (runtimeNamespace !== null) {
@@ -363,6 +368,74 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
return conflicts;
}
function validateReportViewsCheckCatalog(spec: HwlabRuntimeLaneSpec, conflicts: string[], ref: InternalConfigRefStatus | undefined, reportViews: Record<string, unknown> | null): void {
if (reportViews === null) return;
const catalogRef = stringAt(reportViews, "checkCatalogRef");
if (catalogRef === null) {
conflicts.push(`${ref?.file ?? "-"}#${ref?.path ?? "-"}.checkCatalogRef is required`);
return;
}
let catalog: Record<string, unknown>;
try {
const target = readWebProbeSentinelConfigRef(spec, catalogRef).target;
catalog = isRecord(target) ? target : {};
} catch (error) {
conflicts.push(`${ref?.file ?? "-"}#${ref?.path ?? "-"}.checkCatalogRef=${catalogRef} is not readable: ${error instanceof Error ? error.message : String(error)}`);
return;
}
const items = arrayAt(catalog, "items");
if (items.length === 0) {
conflicts.push(`${catalogRef}.items is empty`);
return;
}
const byId = new Map<string, Record<string, unknown>>();
const codes = new Set<string>();
for (const [index, item] of items.entries()) {
const itemId = stringAt(item, "id");
const code = stringAt(item, "code");
if (itemId === null) conflicts.push(`${catalogRef}.items[${index}].id is required`);
else if (byId.has(itemId)) conflicts.push(`${catalogRef}.items id duplicated: ${itemId}`);
else byId.set(itemId, item);
if (code === null) conflicts.push(`${catalogRef}.items[${index}].code is required`);
else if (codes.has(code)) conflicts.push(`${catalogRef}.items code duplicated: ${code}`);
else codes.add(code);
for (const field of ["titleZh", "summaryZh", "actionZh"]) {
const text = stringAt(item, field);
if (text === null) {
conflicts.push(`${catalogRef}.items[${index}].${field} is required`);
} else if (!containsChinese(text)) {
conflicts.push(`${catalogRef}.items[${index}].${field} must contain Chinese text`);
}
}
}
const requiredIds = knownWebProbeFindingIds();
const missing = requiredIds.filter((id) => !byId.has(id));
if (missing.length > 0) {
conflicts.push(`${catalogRef}.items missing analyzer finding ids: ${missing.slice(0, 24).join(",")}${missing.length > 24 ? `,+${missing.length - 24}` : ""}`);
}
}
function knownWebProbeFindingIds(): string[] {
const ids = new Set<string>();
for (const file of [
"scripts/src/hwlab-node-web-observe-analyzer-source.ts",
"scripts/src/hwlab-node-web-sentinel-p5-observe.ts",
]) {
let source = "";
try {
source = readFileSync(rootPath(file), "utf8");
} catch {
continue;
}
for (const match of source.matchAll(/\bid:\s*["`]([A-Za-z0-9_.:-]+)["`]/gu)) ids.add(match[1]);
}
return Array.from(ids).sort();
}
function containsChinese(value: string): boolean {
return /[\u3400-\u9fff]/u.test(value);
}
function requireReadableConfigRef(spec: HwlabRuntimeLaneSpec, conflicts: string[], ref: InternalConfigRefStatus | undefined, path: string, value: unknown): void {
if (typeof value !== "string" || value.length === 0) return;
try {
+20 -24
View File
@@ -552,7 +552,7 @@ await page.evaluate(() => {
await page.waitForTimeout(150);
const trendHoverPoint = await page.evaluate(() => {
const target = document.querySelector(".trend-dot-hit .trend-dot-red") || document.querySelector(".trend-dot-hit .trend-dot-warning");
const target = document.querySelector(".trend-dot-hit .trend-dot-duration");
if (!(target instanceof SVGElement)) return null;
const rect = target.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return null;
@@ -600,17 +600,15 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
const legendTexts = Array.from(document.querySelectorAll(".trend-legend .legend-item")).map((item) => String(item.textContent || "").replace(/\s+/g, " ").trim());
const legendNumber = (label) => {
const row = legendTexts.find((item) => item.includes(label)) || "";
const match = /(\d+)/u.exec(row);
const match = /(\d+(?:\.\d+)?)/u.exec(row);
return match ? Number(match[1]) : null;
};
const chartCounts = {
error: legendNumber("最新点错误"),
warning: legendNumber("最新点告警"),
total: legendNumber("错误+告警合计"),
const chartTiming = {
latestMinutes: legendNumber("最新运行耗时"),
maxMinutes: legendNumber("最近最高耗时"),
title: text("#trend-heading"),
legendHasDuration: legendTexts.some((item) => item.includes("最新运行耗时")),
};
chartCounts.ok = typeof chartCounts.error === "number" && typeof chartCounts.warning === "number" && typeof chartCounts.total === "number"
? chartCounts.total === chartCounts.error + chartCounts.warning
: false;
const apiUrl = (path) => {
const basePath = root?.getAttribute("data-base-path") || expectedRoutePrefix || "";
const prefix = basePath.replace(/\/+$/u, "");
@@ -769,10 +767,9 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
total: errorSampleCount(overviewCounts) + warningSampleCount(overviewCounts),
allSamples: allSampleCount(overviewCounts),
};
chartCounts.matchesLatestRun = chartCounts.ok === true
&& chartCounts.error === latestRunCounts.error
&& chartCounts.warning === latestRunCounts.warning
&& chartCounts.total === latestRunCounts.total;
chartTiming.ok = chartTiming.title.includes("运行耗时") && chartTiming.legendHasDuration === true && Number.isFinite(chartTiming.latestMinutes);
chartTiming.matchesLatestRun = chartTiming.ok === true
&& Math.abs(Number(chartTiming.latestMinutes) - Number(latestRunCounts.durationMinutes)) < 0.02;
const trendPanel = document.querySelector(".trend-panel");
const trendLegend = document.querySelector(".trend-panel .trend-legend");
const trendPanelRect = trendPanel?.getBoundingClientRect();
@@ -870,7 +867,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
trendDotCount: document.querySelectorAll(".trend-dot-hit").length,
trendTooltip: trendTooltipSummary,
trendPanelText: text("#trend-heading"),
chartCounts,
chartTiming,
latestRunCounts,
latestDetailSummary,
timingVisibility,
@@ -881,7 +878,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
overviewSamples,
panelHeights,
scopeLabels: {
latestPointLegend: legendTexts.some((item) => item.includes("最新")),
latestPointLegend: legendTexts.some((item) => item.includes("最新运行耗时")),
historicalSamples: legendTexts.some((item) => item.includes("历史样本累计")) || statusText.includes("历史错误样本"),
},
timelineItems: document.querySelectorAll(".timeline-list .timeline-item").length,
@@ -1067,8 +1064,8 @@ const ok = !navigationError
&& dom.sentinelBoundary?.routePrefixMatches === true
&& dom.errorVisible !== true
&& dom.trendCurve === true
&& dom.chartCounts?.ok === true
&& dom.chartCounts?.matchesLatestRun === true
&& dom.chartTiming?.ok === true
&& dom.chartTiming?.matchesLatestRun === true
&& dom.checkScope?.matchesLatestRun === true
&& dom.checkScope?.matchesRunDetail === true
&& dom.selectedRunTags?.matchesRunDetail === true
@@ -1533,7 +1530,7 @@ function renderDashboardResult(result: Record<string, unknown>): string {
const dom = record(page.dom);
const dataset = record(dom.dataset);
const layout = record(dom.layout);
const chartCounts = record(dom.chartCounts);
const chartTiming = record(dom.chartTiming);
const latestRunCounts = record(dom.latestRunCounts);
const latestDetailSummary = record(dom.latestDetailSummary);
const timingVisibility = record(dom.timingVisibility);
@@ -1570,12 +1567,11 @@ function renderDashboardResult(result: Record<string, unknown>): string {
"",
table(["TITLE", "STATUS_TEXT", "CONTRACT", "BASE_PATH"], [[dom.title, dom.statusText, dataset.contractVersion, dataset.basePath ?? "-"]]),
"",
table(["TREND_ERR_TYPES", "TREND_ALERT_TYPES", "TREND_TOTAL_TYPES", "TREND_EXACT", "MATCH_LATEST", "BAD_TITLE", "BAD_BODY"], [[
chartCounts.error ?? "-",
chartCounts.warning ?? "-",
chartCounts.total ?? "-",
chartCounts.ok ?? "-",
chartCounts.matchesLatestRun ?? "-",
table(["TREND_LATEST_MIN", "TREND_MAX_MIN", "TREND_DURATION", "MATCH_LATEST", "BAD_TITLE", "BAD_BODY"], [[
chartTiming.latestMinutes ?? "-",
chartTiming.maxMinutes ?? "-",
chartTiming.ok ?? "-",
chartTiming.matchesLatestRun ?? "-",
dom.badCardTitleCount ?? "-",
dom.badCardBodyCount ?? "-",
]]),
@@ -1316,6 +1316,7 @@ function enrichFindingWithCheck(config: WebProbeSentinelServiceConfig, value: Re
checkLevel,
checkTitleZh: stringOrNull(check.titleZh),
checkSummaryZh: stringOrNull(check.summaryZh),
checkActionZh: stringOrNull(check.actionZh),
checkRegistered: check.registered === true,
blocking: value.blocking === true || check.blocking === true,
valuesRedacted: true,
@@ -1327,13 +1328,13 @@ function checkForFinding(config: WebProbeSentinelServiceConfig, value: Record<st
const entry = checkCatalogById(config).get(findingId) ?? null;
const rawLevel = normalizeCheckLevel(stringOrNull(value.severity) ?? stringOrNull(value.level));
if (entry === null) {
const summary = stringOrNull(value.summary) ?? stringOrNull(value.message) ?? findingId;
return {
id: findingId,
code: null,
level: rawLevel ?? "unknown",
titleZh: findingId,
summaryZh: summary.slice(0, 160),
titleZh: "未登记监测项",
summaryZh: "该监测项未在 YAML 中登记,配置校验需要补齐中文说明。",
actionZh: "补齐 YAML 监测项说明后重新验证。",
blocking: value.blocking === true,
order: 10000,
registered: false,
@@ -1345,7 +1346,8 @@ function checkForFinding(config: WebProbeSentinelServiceConfig, value: Record<st
code: stringOrNull(entry.code),
level: normalizeCheckLevel(stringOrNull(entry.level)) ?? rawLevel ?? "unknown",
titleZh: stringOrNull(entry.titleZh) ?? findingId,
summaryZh: stringOrNull(entry.summaryZh) ?? stringOrNull(value.summary) ?? stringOrNull(value.message) ?? findingId,
summaryZh: stringOrNull(entry.summaryZh) ?? "已记录监测项详情。",
actionZh: stringOrNull(entry.actionZh) ?? "查看详情后处理。",
blocking: entry.blocking === true,
order: numberOr(entry.order, 10000),
registered: true,