fix: separate sentinel check counts from samples
This commit is contained in:
@@ -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({
|
||||
<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>
|
||||
@@ -351,9 +358,10 @@ 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>错误 {{ redCount({ severityCounts: severityTotals }) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch warning"></span>警告 {{ warningCount({ severityCounts: severityTotals }) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch total"></span>错误+警告合计 {{ trendTotalCount({ severityCounts: severityTotals }) }}</span>
|
||||
<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">历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 警告 {{ warningCount({ severityCounts: severityTotals }) }}</span>
|
||||
<span class="legend-item">{{ cadence.alert }}</span>
|
||||
</div>
|
||||
</section>
|
||||
@@ -394,11 +402,11 @@ createApp({
|
||||
<strong>{{ cadence.latestAge >= 0 ? formatDuration(cadence.latestAge) : "-" }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>错误</span>
|
||||
<span>历史错误样本</span>
|
||||
<strong>{{ redCount({ severityCounts: severityTotals }) }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>警告</span>
|
||||
<span>历史警告样本</span>
|
||||
<strong>{{ warningCount({ severityCounts: severityTotals }) }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
@@ -463,7 +471,9 @@ createApp({
|
||||
<h3>摘要</h3>
|
||||
<div class="detail-grid">
|
||||
<div class="metric"><span>状态</span><strong>{{ selectedRun.status || "-" }}</strong></div>
|
||||
<div class="metric"><span>监测项</span><strong>{{ findingCount(selectedRun) }}</strong></div>
|
||||
<div class="metric"><span>监测项类型</span><strong>{{ findingCount(selectedRun) }}</strong></div>
|
||||
<div class="metric"><span>错误/警告样本</span><strong>{{ alertSampleCount(selectedRun) }}</strong></div>
|
||||
<div class="metric"><span>全部样本</span><strong>{{ findingSampleCount(selectedRun) }}</strong></div>
|
||||
<div class="metric"><span>Observer</span><strong>{{ selectedRun.observerId || "-" }}</strong></div>
|
||||
<div class="metric"><span>更新时间</span><strong>{{ formatDate(selectedRun.updatedAt || selectedRun.createdAt) }}</strong></div>
|
||||
</div>
|
||||
@@ -512,7 +522,7 @@ createApp({
|
||||
>
|
||||
<span class="row-line">
|
||||
<span class="severity-line"><span class="severity-dot"></span><span class="check-code">{{ findingCode(item) }}</span><strong>{{ findingTitle(item) }}</strong></span>
|
||||
<span class="tag" :class="severityClass(item)">{{ levelLabel(item) }} · {{ item.runCount || item.count || 1 }}</span>
|
||||
<span class="tag" :class="severityClass(item)">{{ levelLabel(item) }} · {{ findingGroupCountLabel(item) }}</span>
|
||||
</span>
|
||||
<p class="muted">{{ rootCauseText(item) }}</p>
|
||||
<p v-if="item.nextAction" class="muted">处理: {{ item.nextAction }}</p>
|
||||
@@ -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 || "") || "-" },
|
||||
|
||||
@@ -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, unknown>): 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, unknown>): 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 ?? "-"}`,
|
||||
|
||||
@@ -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<string, number>): 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<string, unknown>, latestRun: Record<string, unknown> | null, severityCounts: Record<string, number>): string {
|
||||
if (health.ok !== true) return "degraded";
|
||||
const latestStatus = latestRun === null ? null : stringOrNull(latestRun.status);
|
||||
|
||||
Reference in New Issue
Block a user