From 4c7c103905fd1c9163501ecb33eadd213022170b Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 08:05:40 +0000 Subject: [PATCH] fix: separate sentinel check counts from samples --- .../monitor-web.js | 64 ++++++++++---- scripts/src/hwlab-node-web-sentinel-cicd.ts | 83 ++++++++++++++----- .../src/hwlab-node-web-sentinel-service.ts | 19 ++++- 3 files changed, 126 insertions(+), 40 deletions(-) diff --git a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js index 8f5b0767..e3e31fc3 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js @@ -40,6 +40,7 @@ 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 trendPolylines = computed(() => ({ red: trendPolyline((run) => trendErrorCount(run)), @@ -221,6 +222,7 @@ createApp({ filteredRuns, selectedRun, trendRows, + latestTrendRun, trendPolylines, trendDots, timelineRuns, @@ -237,7 +239,11 @@ createApp({ redCount, warningCount, findingCount, + findingSampleCount, + alertSampleCount, trendTotalCount, + trendErrorCount, + trendWarningCount, severityClass, formatDate, formatAbsoluteDate, @@ -247,6 +253,7 @@ createApp({ findingTitle, findingCode, levelLabel, + findingGroupCountLabel, detailSummaryText, detailSummaryRows, commandSummary, @@ -302,13 +309,13 @@ createApp({
-

错误 / 警告 / 合计曲线

-

按运行更新时间展示最近 {{ trendRows.length }} 次变化

+

错误 / 警告样本曲线

+

按运行更新时间展示最近 {{ trendRows.length }} 次变化,点位为单次运行样本数

{{ cadence.stale ? "非阻塞报警" : "新鲜" }}
- + @@ -351,9 +358,10 @@ createApp({
暂无运行数据
- 错误 {{ redCount({ severityCounts: severityTotals }) }} - 警告 {{ warningCount({ severityCounts: severityTotals }) }} - 错误+警告合计 {{ trendTotalCount({ severityCounts: severityTotals }) }} + 最新点错误 {{ trendErrorCount(latestTrendRun) }} + 最新点警告 {{ trendWarningCount(latestTrendRun) }} + 最新点错误+警告合计 {{ trendTotalCount(latestTrendRun) }} + 历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 警告 {{ warningCount({ severityCounts: severityTotals }) }} {{ cadence.alert }}
@@ -394,11 +402,11 @@ createApp({ {{ cadence.latestAge >= 0 ? formatDuration(cadence.latestAge) : "-" }}
- 错误 + 历史错误样本 {{ redCount({ severityCounts: severityTotals }) }}
- 警告 + 历史警告样本 {{ warningCount({ severityCounts: severityTotals }) }}
@@ -463,7 +471,9 @@ createApp({

摘要

状态{{ selectedRun.status || "-" }}
-
监测项{{ findingCount(selectedRun) }}
+
监测项类型{{ findingCount(selectedRun) }}
+
错误/警告样本{{ alertSampleCount(selectedRun) }}
+
全部样本{{ findingSampleCount(selectedRun) }}
Observer{{ selectedRun.observerId || "-" }}
更新时间{{ formatDate(selectedRun.updatedAt || selectedRun.createdAt) }}
@@ -512,7 +522,7 @@ createApp({ > {{ findingCode(item) }}{{ findingTitle(item) }} - {{ levelLabel(item) }} · {{ item.runCount || item.count || 1 }} + {{ levelLabel(item) }} · {{ findingGroupCountLabel(item) }}

{{ rootCauseText(item) }}

处理: {{ item.nextAction }}

@@ -593,14 +603,27 @@ function trendTotalCount(item) { } function findingCount(item) { - const counts = item?.severityCounts; - if (counts && typeof counts === "object") return Object.values(counts).reduce((sum, value) => sum + number(value), 0); + if (Number.isFinite(Number(item?.findingTypeCount))) return Number(item.findingTypeCount); if (Number.isFinite(Number(item?.findingCount))) return Number(item.findingCount); if (Number.isFinite(Number(item?.finding_count))) return Number(item.finding_count); if (Number.isFinite(Number(item?.count))) return Number(item.count); + const counts = item?.severityCounts; + if (counts && typeof counts === "object") return Object.keys(counts).length; return Object.values(item || {}).reduce((sum, value) => sum + number(value), 0); } +function findingSampleCount(item) { + if (Number.isFinite(Number(item?.findingSampleCount))) return Number(item.findingSampleCount); + const counts = item?.severityCounts; + if (counts && typeof counts === "object") return Object.values(counts).reduce((sum, value) => sum + number(value), 0); + return 0; +} + +function alertSampleCount(item) { + if (Number.isFinite(Number(item?.findingAlertSampleCount))) return Number(item.findingAlertSampleCount); + return redCount(item) + warningCount(item); +} + function severityClass(item) { const explicit = String(item?.maxSeverity || item?.checkLevel || item?.severity || item?.level || "").toLowerCase(); if (["red", "critical", "error", "blocked"].includes(explicit)) return "red"; @@ -678,6 +701,15 @@ function levelLabel(item) { return "未知"; } +function findingGroupCountLabel(item) { + const runCount = Number.isFinite(Number(item?.runCount)) ? Number(item.runCount) : null; + const sampleCount = Number.isFinite(Number(item?.count)) ? Number(item.count) : null; + if (runCount !== null && sampleCount !== null) return `${runCount} 次运行 / ${sampleCount} 样本`; + if (sampleCount !== null) return `${sampleCount} 样本`; + if (runCount !== null) return `${runCount} 次运行`; + return "1 样本"; +} + function findingSearchText(item) { return [ item?.checkCode, @@ -713,9 +745,11 @@ function detailSummaryRows(detail) { { key: "run", label: "运行", value: shortId(run.id || run.runId || detail.runId) }, { key: "scenario", label: "场景", value: run.scenarioId || run.scenario_id || "-" }, { key: "status", label: "状态", value: run.status || "-" }, - { key: "checks", label: "监测项", value: String(run.findingCount ?? run.finding_count ?? 0) }, - { key: "error", label: "错误", value: String(redCount({ severityCounts: counts })) }, - { key: "warning", label: "警告", value: String(warningCount({ severityCounts: counts })) }, + { key: "checks", label: "监测项类型", value: String(findingCount(run)) }, + { key: "alertSamples", label: "错误/警告样本", value: String(alertSampleCount(run)) }, + { key: "allSamples", label: "全部样本", value: String(findingSampleCount(run)) }, + { key: "error", label: "错误样本", value: String(redCount({ severityCounts: counts })) }, + { key: "warning", label: "警告样本", value: String(warningCount({ severityCounts: counts })) }, { key: "observer", label: "Observer", value: run.observerId || run.observer_id || "-" }, { key: "updated", label: "更新时间", value: formatAbsoluteDate(run.updatedAt || run.updated_at || run.createdAt || run.created_at) }, { key: "report", label: "报告", value: shortHash(artifacts.reportJsonSha256 || run.reportJsonSha256 || run.report_json_sha256 || "") || "-" }, diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index ee55e34a..025c7b92 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -2227,9 +2227,13 @@ if (captureScreenshot && screenshotPath) { }); } -const dom = await page.evaluate(() => { +const dom = await page.evaluate(async () => { const visible = (element) => Boolean(element && !element.hidden); const text = (selector) => String(document.querySelector(selector)?.textContent || "").replace(/\s+/g, " ").trim(); + const numberValue = (value) => Number.isFinite(Number(value)) ? Number(value) : 0; + const errorSampleCount = (counts) => numberValue(counts?.red) + numberValue(counts?.critical) + numberValue(counts?.error); + const warningSampleCount = (counts) => numberValue(counts?.warning) + numberValue(counts?.warn) + numberValue(counts?.amber); + const allSampleCount = (counts) => Object.values(counts || {}).reduce((sum, value) => sum + numberValue(value), 0); const root = document.querySelector("#monitor-web-root"); const shell = document.querySelector("[data-monitor-shell='true']"); const error = document.querySelector("#monitor-web-error"); @@ -2262,6 +2266,35 @@ const dom = await page.evaluate(() => { chartCounts.ok = typeof chartCounts.error === "number" && typeof chartCounts.warning === "number" && typeof chartCounts.total === "number" ? chartCounts.total === chartCounts.error + chartCounts.warning : false; + const runsPayload = await fetch("/api/runs?limit=30&sort=updated", { cache: "no-store" }).then((item) => item.json()).catch(() => null); + const overviewPayload = await fetch("/api/overview", { cache: "no-store" }).then((item) => item.json()).catch(() => null); + const runs = Array.isArray(runsPayload?.runs) ? runsPayload.runs : Array.isArray(runsPayload?.items) ? runsPayload.items : []; + const latestRun = runs[0] || null; + const latestCounts = latestRun && latestRun.severityCounts && typeof latestRun.severityCounts === "object" && !Array.isArray(latestRun.severityCounts) + ? latestRun.severityCounts + : {}; + const latestRunCounts = { + runId: latestRun?.id || latestRun?.runId || null, + typeCount: numberValue(latestRun?.findingTypeCount ?? latestRun?.findingCount ?? latestRun?.finding_count), + error: errorSampleCount(latestCounts), + warning: warningSampleCount(latestCounts), + total: errorSampleCount(latestCounts) + warningSampleCount(latestCounts), + allSamples: allSampleCount(latestCounts), + }; + const overviewCounts = overviewPayload?.severityCounts && typeof overviewPayload.severityCounts === "object" && !Array.isArray(overviewPayload.severityCounts) + ? overviewPayload.severityCounts + : {}; + const overviewSamples = { + error: errorSampleCount(overviewCounts), + warning: warningSampleCount(overviewCounts), + 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; + const statusText = text(".status-strip"); const doc = document.documentElement; const body = document.body; const viewport = { width: window.innerWidth, height: window.innerHeight }; @@ -2280,8 +2313,7 @@ const dom = await page.evaluate(() => { if (overflow.length < 5) { overflow.push({ tag: element.tagName.toLowerCase(), - className: String(element.className || "").slice(0, 80), - text: String(element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80), + className: String(element.className || "").slice(0, 40), x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), @@ -2309,17 +2341,19 @@ const dom = await page.evaluate(() => { summaryText: text(".status-strip"), runRows: document.querySelectorAll(".run-list .run-row").length, findingItems: document.querySelectorAll(".finding-list .finding-card").length, - firstCards: cards, badCardTitleCount: badCardTitles.length, badCardBodyCount: badCardBodies.length, - badCardTitles, - badCardBodies, trendCurve: Boolean(trend), trendDotCount: document.querySelectorAll(".trend-dot-hit").length, trendTooltip: tooltipSummary(trendTooltip), trendPanelText: text("#trend-heading"), - legendTexts, chartCounts, + latestRunCounts, + overviewSamples, + scopeLabels: { + latestPointLegend: legendTexts.some((item) => item.includes("最新点")), + historicalSamples: legendTexts.some((item) => item.includes("历史样本累计")) || statusText.includes("历史错误样本"), + }, timelineItems: document.querySelectorAll(".timeline-list .timeline-item").length, timelineVisible: Boolean(timeline), errorVisible: visible(error), @@ -2327,20 +2361,6 @@ const dom = await page.evaluate(() => { scrollModel: { workspace: Boolean(workspace), paneCount: panes.length, - panes: panes.map((pane) => { - const style = window.getComputedStyle(pane); - const rect = pane.getBoundingClientRect(); - return { - className: String(pane.className || ""), - overflowY: style.overflowY, - scrollHeight: pane.scrollHeight, - clientHeight: pane.clientHeight, - x: Math.round(rect.x), - y: Math.round(rect.y), - width: Math.round(rect.width), - height: Math.round(rect.height), - }; - }), independentScroll: panes.length >= 3 && panes.every((pane) => { const style = window.getComputedStyle(pane); return style.overflowY === "auto" || style.overflowY === "scroll"; @@ -2352,7 +2372,7 @@ const dom = await page.evaluate(() => { documentSize, horizontalOverflow: documentSize.width > viewport.width + 1, overflowCount, - overflow, + overflow: overflow.slice(0, 2), }, }; @@ -2407,6 +2427,9 @@ const ok = !navigationError && dom.errorVisible !== true && dom.trendCurve === true && dom.chartCounts?.ok === true + && dom.chartCounts?.matchesLatestRun === true + && dom.scopeLabels?.latestPointLegend === true + && dom.scopeLabels?.historicalSamples === true && (dom.trendDotCount === 0 || (dom.trendTooltip?.visible === true && dom.trendTooltip?.hasValues === true && dom.trendTooltip?.hasTime === true)) && dom.badCardTitleCount === 0 && dom.badCardBodyCount === 0 @@ -4703,6 +4726,8 @@ function renderDashboardResult(result: Record): string { const dataset = record(dom.dataset); const layout = record(dom.layout); const chartCounts = record(dom.chartCounts); + const latestRunCounts = record(dom.latestRunCounts); + const overviewSamples = record(dom.overviewSamples); const screenshot = record(result.screenshot); const remote = record(result.remote); const transport = record(result.transport); @@ -4725,15 +4750,27 @@ function renderDashboardResult(result: Record): string { "", table(["TITLE", "STATUS_TEXT", "CONTRACT", "BASE_PATH"], [[dom.title, dom.statusText, dataset.contractVersion, dataset.basePath ?? "-"]]), "", - table(["TREND_ERROR", "TREND_WARNING", "TREND_TOTAL", "TREND_EXACT", "BAD_TITLE", "BAD_BODY"], [[ + table(["TREND_ERROR", "TREND_WARNING", "TREND_TOTAL", "TREND_EXACT", "MATCH_LATEST", "BAD_TITLE", "BAD_BODY"], [[ chartCounts.error ?? "-", chartCounts.warning ?? "-", chartCounts.total ?? "-", chartCounts.ok ?? "-", + chartCounts.matchesLatestRun ?? "-", dom.badCardTitleCount ?? "-", dom.badCardBodyCount ?? "-", ]]), "", + table(["LATEST_RUN", "TYPE_COUNT", "LATEST_ERR", "LATEST_WARN", "LATEST_TOTAL", "LATEST_ALL", "HIST_ERR", "HIST_WARN"], [[ + latestRunCounts.runId ?? "-", + latestRunCounts.typeCount ?? "-", + latestRunCounts.error ?? "-", + latestRunCounts.warning ?? "-", + latestRunCounts.total ?? "-", + latestRunCounts.allSamples ?? "-", + overviewSamples.error ?? "-", + overviewSamples.warning ?? "-", + ]]), + "", table(["VIEWPORT", "DOC", "H_OVERFLOW", "OVERFLOW_COUNT"], [[ result.viewport, `${record(layout.documentSize).width ?? "-"}x${record(layout.documentSize).height ?? "-"}`, diff --git a/scripts/src/hwlab-node-web-sentinel-service.ts b/scripts/src/hwlab-node-web-sentinel-service.ts index 8c9dfeea..b659c896 100644 --- a/scripts/src/hwlab-node-web-sentinel-service.ts +++ b/scripts/src/hwlab-node-web-sentinel-service.ts @@ -888,6 +888,9 @@ function dashboardRunSummary(config: WebProbeSentinelServiceConfig, db: Database const id = stringOrNull(row.id); const severityCounts = id === null ? {} : severityCountsForRun(config, db, id); const maxSeverity = maxSeverityFromCounts(severityCounts); + const findingTypeCount = numberOr(row.finding_count, 0); + const findingSampleCount = Object.values(severityCounts).reduce((sum, value) => sum + numberOr(value, 0), 0); + const findingAlertSampleCount = alertSeveritySampleCount(severityCounts); return { id, runId: id, @@ -903,8 +906,11 @@ function dashboardRunSummary(config: WebProbeSentinelServiceConfig, db: Database stateDir: stringOrNull(row.state_dir), report_json_sha256: stringOrNull(row.report_json_sha256), reportJsonSha256: stringOrNull(row.report_json_sha256), - finding_count: numberOr(row.finding_count, 0), - findingCount: numberOr(row.finding_count, 0), + finding_count: findingTypeCount, + findingCount: findingTypeCount, + findingTypeCount, + findingSampleCount, + findingAlertSampleCount, artifact_count: numberOr(row.artifact_count, 0), artifactCount: numberOr(row.artifact_count, 0), maintenance: numberOr(row.maintenance, 0) === 1, @@ -921,6 +927,15 @@ function dashboardRunSummary(config: WebProbeSentinelServiceConfig, db: Database }; } +function alertSeveritySampleCount(counts: Record): number { + return numberOr(counts.red, 0) + + numberOr(counts.critical, 0) + + numberOr(counts.error, 0) + + numberOr(counts.warning, 0) + + numberOr(counts.warn, 0) + + numberOr(counts.amber, 0); +} + function dashboardOverallStatus(health: Record, latestRun: Record | null, severityCounts: Record): string { if (health.ok !== true) return "degraded"; const latestStatus = latestRun === null ? null : stringOrNull(latestRun.status);