diff --git a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.css b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.css index 0f9b1d82..2f7db17d 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.css +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.css @@ -123,7 +123,7 @@ code, .metric, .timeline-item, .run-row, -.finding-card, +.check-table, .check-chip { border: 1px solid var(--line); border-radius: 8px; @@ -277,7 +277,8 @@ select { grid-template-columns: minmax(420px, 1.55fr) minmax(300px, 0.9fr); gap: 10px; flex: 0 0 auto; - min-height: 212px; + align-items: start; + min-height: 0; } .trend-panel, @@ -296,6 +297,10 @@ select { padding: 12px; } +.trend-panel { + align-self: start; +} + .panel-header { display: flex; align-items: start; @@ -317,7 +322,6 @@ select { .trend-chart-wrap { position: relative; - min-height: 142px; border: 1px solid var(--line); border-radius: 8px; background: linear-gradient(180deg, #ffffff 0%, #f7faf9 100%); @@ -327,7 +331,7 @@ select { .trend-chart { display: block; width: 100%; - height: 142px; + height: clamp(118px, 13vw, 150px); } .trend-empty { @@ -470,16 +474,25 @@ select { .timeline-item { display: grid; - grid-template-columns: 78px minmax(0, 1fr) auto; + grid-template-columns: 78px minmax(0, 1fr) minmax(112px, auto); gap: 8px; align-items: center; padding: 7px 8px; font-size: 12px; } +.run-alert-tags { + display: flex; + min-width: 0; + flex: 0 0 auto; + flex-wrap: wrap; + justify-content: flex-end; + gap: 4px; +} + .timeline-item strong, .run-row strong, -.finding-card strong { +.check-title-cell strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; @@ -495,19 +508,19 @@ select { .timeline-item.red .timeline-marker, .run-row.red .severity-dot, -.finding-card.red .severity-dot { +.check-row.red .severity-dot { background: var(--red); } .timeline-item.warning .timeline-marker, .run-row.warning .severity-dot, -.finding-card.warning .severity-dot { +.check-row.warning .severity-dot { background: var(--amber); } .timeline-item.info .timeline-marker, .run-row.info .severity-dot, -.finding-card.info .severity-dot { +.check-row.info .severity-dot { background: var(--blue); } @@ -652,14 +665,10 @@ select { } .finding-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); - align-content: start; - gap: 8px; + min-height: 0; } -.run-row, -.finding-card { +.run-row { display: grid; gap: 6px; padding: 10px; @@ -670,6 +679,80 @@ select { cursor: pointer; } +.check-table-wrap { + overflow: auto; + border: 1px solid var(--line); + border-radius: 8px; + background: #ffffff; +} + +.check-table { + width: 100%; + min-width: 1120px; + border-collapse: separate; + border-spacing: 0; + font-size: 12px; +} + +.check-table th, +.check-table td { + border-bottom: 1px solid var(--line); + padding: 9px 10px; + text-align: left; + vertical-align: middle; +} + +.check-table th { + position: sticky; + top: 0; + z-index: 2; + background: #f7faf9; + color: var(--muted); + font-weight: 600; +} + +.check-table tr:last-child td { + border-bottom: 0; +} + +.check-row { + cursor: pointer; +} + +.check-row:hover, +.check-row:focus-visible { + background: #f3f8f7; + outline: none; +} + +.check-row.red { + box-shadow: inset 3px 0 0 var(--red); +} + +.check-row.warning { + box-shadow: inset 3px 0 0 var(--amber); +} + +.check-title-cell { + min-width: 260px; +} + +.check-title-cell strong, +.check-title-cell span { + display: block; +} + +.check-title-cell span { + margin-top: 3px; + color: var(--muted); + line-height: 1.35; +} + +.detail-link { + color: var(--blue); + font-weight: 600; +} + .run-row.selected { border-color: var(--blue); box-shadow: inset 3px 0 0 var(--blue); @@ -732,6 +815,83 @@ select { color: #73500f; } +.tag.healthy { + background: var(--green-soft); + color: #17633f; +} + +.check-dialog-backdrop { + position: fixed; + inset: 0; + z-index: 50; + display: grid; + place-items: center; + background: rgba(17, 32, 30, 0.42); + padding: 24px; +} + +.check-dialog { + display: flex; + width: min(1120px, 96vw); + max-height: 88dvh; + min-height: min(520px, 88dvh); + flex-direction: column; + overflow: hidden; + border: 1px solid var(--line-strong); + border-radius: 8px; + background: var(--panel); + box-shadow: 0 24px 70px rgba(17, 32, 30, 0.24); +} + +.check-dialog-header { + display: flex; + flex: 0 0 auto; + align-items: start; + justify-content: space-between; + gap: 16px; + border-bottom: 1px solid var(--line); + padding: 16px; +} + +.check-dialog-header h2 { + margin-top: 8px; + font-size: 20px; +} + +.check-dialog-actions { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 8px; +} + +.dialog-close { + min-height: 32px; + border: 1px solid var(--line-strong); + border-radius: 8px; + background: #ffffff; + color: var(--text); + padding: 0 12px; + cursor: pointer; +} + +.check-dialog-body { + display: grid; + grid-template-columns: minmax(300px, 0.82fr) minmax(420px, 1fr); + gap: 12px; + min-height: 0; + overflow: auto; + padding: 16px; +} + +.check-dialog-body .detail-card { + min-width: 0; +} + +.detail-card-wide { + grid-column: 1 / -1; +} + .detail-card { border: 1px solid var(--line); border-radius: 8px; @@ -891,6 +1051,10 @@ pre { max-height: 80dvh; } + .check-dialog-body { + grid-template-columns: 1fr; + } + .status-strip, .check-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -930,8 +1094,26 @@ pre { grid-template-columns: 62px minmax(0, 1fr); } - .timeline-item .tag { + .timeline-item .run-alert-tags { grid-column: 2; - width: max-content; + justify-content: flex-start; + } + + .check-dialog-backdrop { + padding: 8px; + } + + .check-dialog { + width: 100%; + max-height: 94dvh; + } + + .check-dialog-header { + flex-direction: column; + } + + .check-dialog-actions { + width: 100%; + justify-content: space-between; } } 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 a1d05fc1..de62c44d 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js @@ -104,6 +104,7 @@ createApp({ const overview = ref(null); const runs = ref([]); const findings = ref([]); + const runCheckSummaries = ref({}); const selectedRunId = ref(""); const selectedDetail = ref(null); const runFilter = ref(""); @@ -113,6 +114,7 @@ createApp({ const checkTimeWindow = ref("24h"); const checkSeverityFilter = ref("alert"); const findingFilter = ref(""); + const activeCheckItem = ref(null); const autoRefresh = ref(true); const refreshSeconds = ref(30); const lastLoadedAt = ref(""); @@ -169,7 +171,7 @@ createApp({ timeLabel: formatDate(rawTime), absoluteTime: formatAbsoluteDate(rawTime), reportSha: shortHash(run.reportJsonSha256 || run.report_json_sha256 || run.reportSha256 || ""), - title: `${shortId(run.id)} ${formatAbsoluteDate(rawTime)} 错误 ${red} 警告 ${warning} 合计 ${total}`, + title: `${shortId(run.id)} ${formatAbsoluteDate(rawTime)} 错误 ${red} 告警 ${warning} 合计 ${total}`, }; })); const timelineRuns = computed(() => runs.value.slice(0, 16)); @@ -245,11 +247,12 @@ createApp({ } else { console.warn("monitor-web findings refresh failed", findingsResult.reason); } + await refreshRunCheckSummaries(runs.value); lastLoadedAt.value = new Date().toISOString(); lastAutoRefreshAt = Date.now(); const keepSelected = runs.value.find((run) => run.id === selectedRunId.value); const nextRun = keepSelected || runs.value[0] || latestRun.value; - if (nextRun?.id) void selectRun(nextRun, true); + if (nextRun?.id) await selectRun(nextRun, true); } catch (cause) { const message = String(cause?.message || cause); if (!options.silent || runs.value.length === 0) error.value = message; @@ -282,6 +285,63 @@ createApp({ if (run) void selectRun(run); } + async function refreshRunCheckSummaries(rows) { + const targets = Array.isArray(rows) ? rows.slice(0, 48) : []; + const next = { ...runCheckSummaries.value }; + const results = await Promise.allSettled(targets.map(async (run) => { + const runId = run?.id || run?.runId; + if (!runId) return null; + const payload = await fetchJson(`/api/runs/${encodeURIComponent(runId)}`); + const detailRows = Array.isArray(payload.findings) ? payload.findings : []; + return [runId, summarizeCheckRows(detailRows)]; + })); + const failures = results.filter((item) => item.status === "rejected"); + if (failures.length > 0) throw new Error(`监测项详情加载失败: ${failures.length}`); + for (const result of results) { + if (result.status !== "fulfilled" || result.value === null) continue; + const [runId, summary] = result.value; + next[runId] = summary; + } + runCheckSummaries.value = next; + } + + function trendSummary(run) { + const runId = run?.id || run?.runId || ""; + return runId ? runCheckSummaries.value[runId] || emptyCheckSummary() : emptyCheckSummary(); + } + + function runCheckErrorCount(run) { + return trendSummary(run).errorTypeCount; + } + + function runCheckWarningCount(run) { + return trendSummary(run).warningTypeCount; + } + + function runCheckAlertCount(run) { + return trendSummary(run).alertTypeCount; + } + + function trendErrorCount(run) { + return runCheckErrorCount(run); + } + + function trendWarningCount(run) { + return runCheckWarningCount(run); + } + + function trendTotalCount(run) { + return runCheckAlertCount(run); + } + + function openCheckDetail(item) { + activeCheckItem.value = item || null; + } + + function closeCheckDetail() { + activeCheckItem.value = null; + } + async function refreshHistoricalFindings() { try { const findingsPayload = await fetchJson(findingsApiPath(checkTimeWindow.value)); @@ -347,6 +407,7 @@ createApp({ overview, runs, findings, + runCheckSummaries, selectedRunId, selectedDetail, runFilter, @@ -356,6 +417,7 @@ createApp({ checkTimeWindow, checkSeverityFilter, findingFilter, + activeCheckItem, autoRefresh, refreshSeconds, lastLoadedAt, @@ -384,6 +446,8 @@ createApp({ loadAll, selectRun, selectCheckRun, + openCheckDetail, + closeCheckDetail, refreshNow, currentHref, showTrendTooltip, @@ -393,6 +457,9 @@ createApp({ findingCount, findingSampleCount, alertSampleCount, + runCheckErrorCount, + runCheckWarningCount, + runCheckAlertCount, trendTotalCount, trendErrorCount, trendWarningCount, @@ -404,6 +471,12 @@ createApp({ rootCauseText, findingTitle, findingCode, + checkRowKey, + checkRunText, + checkTimeText, + checkActionText, + checkDetailRows, + checkEvidenceRows, levelLabel, findingGroupCountLabel, timeWindowLabel, @@ -462,13 +535,13 @@ createApp({
-

错误 / 警告样本曲线

-

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

+

错误 / 告警监测项曲线

+

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

{{ cadence.stale ? "非阻塞报警" : "新鲜" }}
- + @@ -505,16 +578,16 @@ createApp({ {{ shortId(hoveredTrendDot.runId) }} {{ hoveredTrendDot.absoluteTime }} 状态 {{ hoveredTrendDot.status }} - 错误 {{ hoveredTrendDot.red }} / 警告 {{ hoveredTrendDot.warning }} / 合计 {{ hoveredTrendDot.total }} + 错误 {{ hoveredTrendDot.red }} / 告警 {{ hoveredTrendDot.warning }} / 合计 {{ hoveredTrendDot.total }} report {{ hoveredTrendDot.reportSha }}
暂无运行数据
最新点错误 {{ trendErrorCount(latestTrendRun) }} - 最新点警告 {{ trendWarningCount(latestTrendRun) }} - 最新点错误+警告合计 {{ trendTotalCount(latestTrendRun) }} - 历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 警告 {{ warningCount({ severityCounts: severityTotals }) }} + 最新点告警 {{ trendWarningCount(latestTrendRun) }} + 最新点错误+告警合计 {{ trendTotalCount(latestTrendRun) }} + 历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 告警 {{ warningCount({ severityCounts: severityTotals }) }} {{ cadence.alert }}
@@ -538,7 +611,11 @@ createApp({ > {{ formatDate(run.updatedAt || run.createdAt) }} {{ run.scenarioId || shortId(run.id) }} - {{ findingCount(run) }} 项 + + 错误 {{ runCheckErrorCount(run) }} + 告警 {{ runCheckWarningCount(run) }} + 正常 +
暂无时间线记录
@@ -559,7 +636,7 @@ createApp({ {{ redCount({ severityCounts: severityTotals }) }}
- 历史警告样本 + 历史告警样本 {{ warningCount({ severityCounts: severityTotals }) }}
@@ -584,7 +661,7 @@ createApp({ @@ -600,7 +677,11 @@ createApp({ > {{ run.scenarioId || shortId(run.id) }} - {{ findingCount(run) }} + + 错误 {{ runCheckErrorCount(run) }} + 告警 {{ runCheckWarningCount(run) }} + 正常 + {{ run.status || "-" }} @@ -625,7 +706,7 @@ createApp({
状态{{ selectedRun.status || "-" }}
监测项类型{{ findingCount(selectedRun) }}
-
错误/警告样本{{ alertSampleCount(selectedRun) }}
+
错误/告警样本{{ alertSampleCount(selectedRun) }}
全部样本{{ findingSampleCount(selectedRun) }}
Observer{{ selectedRun.observerId || "-" }}
更新时间{{ formatDate(selectedRun.updatedAt || selectedRun.createdAt) }}
@@ -663,6 +744,8 @@ createApp({ :data-check-scope="checkScope" :data-check-run-id="checkScopeRun ? checkScopeRun.id : ''" :data-check-type-count="scopedCheckSummary.typeCount" + :data-check-error-type-count="scopedCheckSummary.errorTypeCount" + :data-check-warning-type-count="scopedCheckSummary.warningTypeCount" :data-check-alert-type-count="scopedCheckSummary.alertTypeCount" :data-check-error-samples="scopedCheckSummary.errorSamples" :data-check-warning-samples="scopedCheckSummary.warningSamples" @@ -675,7 +758,7 @@ createApp({

监测项

{{ checkScopeText }}

- 错误/警告样本 {{ scopedCheckSummary.alertSamples }} + 错误/告警样本 {{ scopedCheckSummary.alertSamples }}
@@ -703,28 +786,101 @@ createApp({
当前作用域{{ checkScope === "history" ? timeWindowLabel(checkTimeWindow) : shortId(checkScopeRun && checkScopeRun.id) }}
监测项类型{{ scopedCheckSummary.typeCount }}
-
错误/警告类型{{ scopedCheckSummary.alertTypeCount }}
+
错误/告警类型{{ scopedCheckSummary.alertTypeCount }}
错误样本{{ scopedCheckSummary.errorSamples }}
-
警告样本{{ scopedCheckSummary.warningSamples }}
-
错误/警告样本{{ scopedCheckSummary.alertSamples }}
+
告警样本{{ scopedCheckSummary.warningSamples }}
+
错误/告警样本{{ scopedCheckSummary.alertSamples }}
-
-
- - {{ findingCode(item) }}{{ findingTitle(item) }} - {{ levelLabel(item) }} · {{ findingGroupCountLabel(item) }} - -

{{ rootCauseText(item) }}

-

处理: {{ item.nextAction }}

-
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
编号等级标题样本运行记录时间处理建议详情
{{ findingCode(item) }}{{ levelLabel(item) }}{{ findingTitle(item) }}{{ rootCauseText(item) }}{{ findingGroupCountLabel(item) }}{{ checkRunText(item) }}{{ checkTimeText(item) }}{{ checkActionText(item) }}查看
暂无匹配监测项
+ +
+ +
`, }).mount("#monitor-web-root"); @@ -775,27 +931,6 @@ function warningCount(item) { return number(counts.warning) + number(counts.warn) + number(counts.amber); } -function trendSeverityCounts(item) { - const counts = item?.severityCounts; - return counts && typeof counts === "object" && !Array.isArray(counts) ? counts : null; -} - -function trendErrorCount(item) { - const counts = trendSeverityCounts(item); - if (!counts) return 0; - return number(counts.red) + number(counts.critical) + number(counts.error); -} - -function trendWarningCount(item) { - const counts = trendSeverityCounts(item); - if (!counts) return 0; - return number(counts.warning) + number(counts.warn) + number(counts.amber); -} - -function trendTotalCount(item) { - return trendErrorCount(item) + trendWarningCount(item); -} - function findingCount(item) { if (Number.isFinite(Number(item?.findingTypeCount))) return Number(item.findingTypeCount); if (Number.isFinite(Number(item?.findingCount))) return Number(item.findingCount); @@ -836,6 +971,19 @@ function summarizeCheckRows(rows) { }; } +function emptyCheckSummary() { + return { + typeCount: 0, + errorTypeCount: 0, + warningTypeCount: 0, + alertTypeCount: 0, + errorSamples: 0, + warningSamples: 0, + alertSamples: 0, + allSamples: 0, + }; +} + function checkMatchesLevel(item, filter) { const value = String(filter || "alert"); const bucket = severityBucket(item); @@ -931,7 +1079,7 @@ function levelLabel(item) { const value = String(item?.checkLevel || item?.severity || item?.level || "").toLowerCase(); if (["critical", "red"].includes(value)) return "严重"; if (["error", "blocked", "failed"].includes(value)) return "错误"; - if (["warning", "warn", "amber"].includes(value)) return "警告"; + if (["warning", "warn", "amber"].includes(value)) return "告警"; if (["info", "notice"].includes(value)) return "信息"; return "未知"; } @@ -965,6 +1113,61 @@ function findingSearchText(item) { ].filter((value) => value !== null && value !== undefined).join(" ").toLowerCase(); } +function checkRowKey(item, index) { + return [ + findingCode(item), + item?.latestRunId || item?.runId || item?.run?.id || "", + item?.sampleSeq ?? item?.count ?? index, + ].join(":"); +} + +function checkRunText(item) { + return shortId(item?.latestRunId || item?.runId || item?.run?.id || item?.scenarioId || ""); +} + +function checkTimeText(item) { + const value = item?.latestRunUpdatedAt || item?.updatedAt || item?.updated_at || item?.createdAt || item?.created_at || item?.timestamp || ""; + return value ? formatDate(value) : "-"; +} + +function checkActionText(item) { + return safeUserText(item?.nextAction || item?.action || item?.recommendation) || "查看详情后处理"; +} + +function checkDetailRows(item) { + if (!item) return [{ key: "empty", label: "状态", value: "未选择监测项" }]; + return [ + { key: "code", label: "编号", value: findingCode(item) }, + { key: "level", label: "等级", value: levelLabel(item) }, + { key: "samples", label: "样本", value: findingGroupCountLabel(item) }, + { key: "run", label: "运行记录", value: checkRunText(item) }, + { key: "time", label: "时间", value: checkTimeText(item) }, + { key: "scenario", label: "场景", value: safeDetailValue(item?.scenarioId || item?.scenario_id) }, + { key: "observer", label: "观察任务", value: safeDetailValue(item?.observerId || item?.observer_id) }, + { key: "report", label: "报告", value: shortHash(item?.reportJsonSha256 || item?.report_json_sha256 || item?.reportSha256 || "") || "-" }, + ].filter((row) => row.value !== ""); +} + +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: "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) }, + { key: "range", label: "采集范围", value: safeDetailValue(item?.sentinelRange || evidence.sentinelRange) }, + { key: "blocking", label: "阻塞状态", value: item?.blocking === true ? "阻塞" : "非阻塞" }, + ].filter((row) => row.value !== "" && row.value !== "-"); + return rows.length > 0 ? rows : [{ key: "none", label: "证据摘要", value: "已记录到报告详情。" }]; +} + +function safeDetailValue(value) { + if (value === null || value === undefined || value === "") return "-"; + const text = String(value).replace(/\s+/g, " ").trim(); + return text.length > 0 ? text : "-"; +} + function checkDisplay(item) { const rawCode = rawCheckCode(item); const registered = checkDisplayCatalog[rawCode]; @@ -1009,10 +1212,10 @@ function detailSummaryRows(detail) { { key: "scenario", label: "场景", value: run.scenarioId || run.scenario_id || "-" }, { key: "status", label: "状态", value: run.status || "-" }, { key: "checks", label: "监测项类型", value: String(findingCount(run)) }, - { key: "alertSamples", label: "错误/警告样本", value: String(alertSampleCount(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: "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 || "") || "-" }, @@ -1033,7 +1236,7 @@ function statusLabel(status) { const value = String(status || ""); if (value === "blocked") return "阻塞"; if (value === "degraded") return "降级"; - if (value === "warning") return "警告"; + if (value === "warning") return "告警"; if (value === "healthy") return "健康"; return "空闲"; } diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index ef77da98..f3e9ce97 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -2266,10 +2266,11 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId const detailHeader = detailPane?.querySelector(".pane-header"); const checksHeader = checksPanel?.querySelector(".pane-header"); 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 cards = Array.from(document.querySelectorAll(".finding-card")).slice(0, 8).map((card) => ({ - code: String(card.querySelector(".check-code")?.textContent || "").trim(), - title: String(card.querySelector("strong")?.textContent || "").trim(), - body: String(card.textContent || "").replace(/\s+/g, " ").trim().slice(0, 180), + const checkRows = Array.from(document.querySelectorAll("[data-check-row='true']")); + const cards = checkRows.slice(0, 8).map((row) => ({ + code: String(row.querySelector(".check-code")?.textContent || "").trim(), + title: String(row.querySelector(".check-title-cell strong")?.textContent || row.querySelector("strong")?.textContent || "").trim(), + body: String(row.textContent || "").replace(/\s+/g, " ").trim().slice(0, 180), })); const badCardTitles = cards.filter((card) => internalTextPattern.test(card.title)); const badCardBodies = cards.filter((card) => internalTextPattern.test(card.body)); @@ -2280,9 +2281,9 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId return match ? Number(match[1]) : null; }; const chartCounts = { - error: legendNumber("错误"), - warning: legendNumber("警告"), - total: legendNumber("错误+警告合计"), + error: legendNumber("最新点错误"), + warning: legendNumber("最新点告警"), + total: legendNumber("错误+告警合计"), }; chartCounts.ok = typeof chartCounts.error === "number" && typeof chartCounts.warning === "number" && typeof chartCounts.total === "number" ? chartCounts.total === chartCounts.error + chartCounts.warning @@ -2302,9 +2303,13 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId 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), + error: 0, + warning: 0, + total: 0, + all: 0, + errorSamples: errorSampleCount(latestCounts), + warningSamples: warningSampleCount(latestCounts), + alertSamples: errorSampleCount(latestCounts) + warningSampleCount(latestCounts), allSamples: allSampleCount(latestCounts), }; const latestDetailPayload = latestRunCounts.runId @@ -2325,6 +2330,8 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId const sum = (items) => items.reduce((total, row) => total + sampleCount(row), 0); return { typeCount: rows.length, + errorTypeCount: errorRows.length, + warningTypeCount: warningRows.length, alertTypeCount: errorRows.length + warningRows.length, errorSamples: sum(errorRows), warningSamples: sum(warningRows), @@ -2332,6 +2339,11 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId }; }; const latestDetailSummary = summarizeRows(latestDetailRows); + latestRunCounts.typeCount = latestDetailSummary.typeCount; + latestRunCounts.error = latestDetailSummary.errorTypeCount; + latestRunCounts.warning = latestDetailSummary.warningTypeCount; + latestRunCounts.total = latestDetailSummary.alertTypeCount; + latestRunCounts.all = latestDetailSummary.typeCount; const workspaceRect = workspace?.getBoundingClientRect(); const checksRect = checksPanel?.getBoundingClientRect(); const heightSummary = (rect) => { @@ -2368,30 +2380,46 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId scope: checksPanel?.getAttribute("data-check-scope") || null, runId: checksPanel?.getAttribute("data-check-run-id") || null, typeCount: numberValue(checksPanel?.getAttribute("data-check-type-count")), + errorTypeCount: numberValue(checksPanel?.getAttribute("data-check-error-type-count")), + warningTypeCount: numberValue(checksPanel?.getAttribute("data-check-warning-type-count")), alertTypeCount: numberValue(checksPanel?.getAttribute("data-check-alert-type-count")), errorSamples: numberValue(checksPanel?.getAttribute("data-check-error-samples")), warningSamples: numberValue(checksPanel?.getAttribute("data-check-warning-samples")), alertSamples: numberValue(checksPanel?.getAttribute("data-check-alert-samples")), - visibleCardCount: document.querySelectorAll(".finding-list .finding-card").length, + visibleRowCount: document.querySelectorAll("[data-check-row='true']").length, visibleAlertSamples: numberValue(checksPanel?.getAttribute("data-visible-check-alert-samples")), matchesLatestRun: false, matchesRunDetail: false, belowWorkspace: Boolean(workspaceRect && checksRect && checksRect.top >= workspaceRect.bottom - 2), fullWidth: Boolean(workspaceRect && checksRect && checksRect.width >= workspaceRect.width - 2), }; + const selectedRunRow = document.querySelector(".run-list .run-row.selected"); + const selectedRunTags = { + error: numberValue(String(selectedRunRow?.querySelector("[data-run-error-tag='true']")?.textContent || "").match(/(\\d+)/u)?.[1]), + warning: numberValue(String(selectedRunRow?.querySelector("[data-run-warning-tag='true']")?.textContent || "").match(/(\\d+)/u)?.[1]), + errorVisible: Boolean(selectedRunRow?.querySelector("[data-run-error-tag='true']")), + warningVisible: Boolean(selectedRunRow?.querySelector("[data-run-warning-tag='true']")), + matchesRunDetail: false, + }; + selectedRunTags.matchesRunDetail = selectedRunTags.error === latestDetailSummary.errorTypeCount + && selectedRunTags.warning === latestDetailSummary.warningTypeCount + && selectedRunTags.errorVisible === (latestDetailSummary.errorTypeCount > 0) + && selectedRunTags.warningVisible === (latestDetailSummary.warningTypeCount > 0); checkScope.matchesLatestRun = checkScope.present === true && checkScope.scope === "run" && checkScope.runId === latestRunCounts.runId - && checkScope.errorSamples === latestRunCounts.error - && checkScope.warningSamples === latestRunCounts.warning - && checkScope.alertSamples === latestRunCounts.total; + && checkScope.errorTypeCount === latestRunCounts.error + && checkScope.warningTypeCount === latestRunCounts.warning + && checkScope.alertTypeCount === latestRunCounts.total; checkScope.matchesRunDetail = checkScope.present === true && checkScope.typeCount === latestDetailSummary.typeCount + && checkScope.errorTypeCount === latestDetailSummary.errorTypeCount + && checkScope.warningTypeCount === latestDetailSummary.warningTypeCount && checkScope.alertTypeCount === latestDetailSummary.alertTypeCount && checkScope.errorSamples === latestDetailSummary.errorSamples && checkScope.warningSamples === latestDetailSummary.warningSamples && checkScope.alertSamples === latestDetailSummary.alertSamples - && checkScope.visibleCardCount === latestDetailSummary.alertTypeCount + && checkScope.visibleRowCount === latestDetailSummary.alertTypeCount && checkScope.visibleAlertSamples === latestDetailSummary.alertSamples; const overviewCounts = overviewPayload?.severityCounts && typeof overviewPayload.severityCounts === "object" && !Array.isArray(overviewPayload.severityCounts) ? overviewPayload.severityCounts @@ -2406,6 +2434,32 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId && chartCounts.error === latestRunCounts.error && chartCounts.warning === latestRunCounts.warning && chartCounts.total === latestRunCounts.total; + const trendPanel = document.querySelector(".trend-panel"); + const trendLegend = document.querySelector(".trend-panel .trend-legend"); + const trendPanelRect = trendPanel?.getBoundingClientRect(); + const trendLegendRect = trendLegend?.getBoundingClientRect(); + const trendPanelCompact = { + present: Boolean(trendPanelRect && trendLegendRect), + bottomSlackPx: trendPanelRect && trendLegendRect ? Math.round(trendPanelRect.bottom - trendLegendRect.bottom) : null, + ok: Boolean(trendPanelRect && trendLegendRect && trendPanelRect.bottom - trendLegendRect.bottom <= 28), + }; + const firstCheckRow = document.querySelector("[data-check-row='true']"); + let checkDialog = { opened: false, title: "", width: null, height: null, large: false }; + if (firstCheckRow instanceof HTMLElement) { + firstCheckRow.click(); + await new Promise((resolve) => window.setTimeout(resolve, 80)); + const dialog = document.querySelector("[data-check-dialog='true'] .check-dialog"); + const rect = dialog?.getBoundingClientRect(); + checkDialog = { + opened: Boolean(dialog), + title: String(dialog?.querySelector("#check-dialog-title")?.textContent || "").trim(), + width: rect ? Math.round(rect.width) : null, + height: rect ? Math.round(rect.height) : null, + large: Boolean(rect && rect.width >= Math.min(900, window.innerWidth * 0.7) && rect.height >= Math.min(460, window.innerHeight * 0.5)), + }; + const close = dialog?.querySelector("button[aria-label='关闭监测项详情']"); + if (close instanceof HTMLElement) close.click(); + } const datasetSentinelId = root?.getAttribute("data-sentinel-id") || ""; const finalPath = new URL(window.location.href).pathname.replace(/\/+$/u, "") || "/"; const expectedPath = expectedRoutePrefix.replace(/\/+$/u, "") || "/"; @@ -2470,7 +2524,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId subtitle: text(".subtitle"), summaryText: text(".status-strip"), runRows: document.querySelectorAll(".run-list .run-row").length, - findingItems: document.querySelectorAll(".finding-list .finding-card").length, + checkRows: document.querySelectorAll("[data-check-row='true']").length, badCardTitleCount: badCardTitles.length, badCardBodyCount: badCardBodies.length, trendCurve: Boolean(trend), @@ -2481,6 +2535,9 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId latestRunCounts, latestDetailSummary, checkScope, + selectedRunTags, + trendPanelCompact, + checkDialog, overviewSamples, panelHeights, scopeLabels: { @@ -2515,7 +2572,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId return { visible: Boolean(element && body.length > 0), text: body.slice(0, 240), - hasValues: /错误\s+\d+/u.test(body) && /警告\s+\d+/u.test(body) && /合计\s+\d+/u.test(body), + hasValues: /错误\s+\d+/u.test(body) && /告警\s+\d+/u.test(body) && /合计\s+\d+/u.test(body), hasTime: /UTC/u.test(body) || /\d{4}-\d{2}-\d{2}/u.test(body), }; } @@ -2573,6 +2630,8 @@ const runFilterProbe = await page.evaluate(async ({ expectedRoutePrefix }) => { const sum = (items) => items.reduce((total, row) => total + sampleCount(row), 0); return { typeCount: rows.length, + errorTypeCount: errorRows.length, + warningTypeCount: warningRows.length, alertTypeCount: errorRows.length + warningRows.length, errorSamples: sum(errorRows), warningSamples: sum(warningRows), @@ -2581,16 +2640,21 @@ const runFilterProbe = await page.evaluate(async ({ expectedRoutePrefix }) => { }; const panelCounts = () => { const panel = document.querySelector("[data-monitor-checks='true']"); + const selectedRunRow = document.querySelector(".run-list .run-row.selected"); return { present: Boolean(panel), runId: panel?.getAttribute("data-check-run-id") || null, typeCount: numberValue(panel?.getAttribute("data-check-type-count")), + errorTypeCount: numberValue(panel?.getAttribute("data-check-error-type-count")), + warningTypeCount: numberValue(panel?.getAttribute("data-check-warning-type-count")), alertTypeCount: numberValue(panel?.getAttribute("data-check-alert-type-count")), errorSamples: numberValue(panel?.getAttribute("data-check-error-samples")), warningSamples: numberValue(panel?.getAttribute("data-check-warning-samples")), alertSamples: numberValue(panel?.getAttribute("data-check-alert-samples")), - visibleCardCount: document.querySelectorAll(".finding-list .finding-card").length, + visibleRowCount: document.querySelectorAll("[data-check-row='true']").length, visibleAlertSamples: numberValue(panel?.getAttribute("data-visible-check-alert-samples")), + selectedRunErrorTag: numberValue(String(selectedRunRow?.querySelector("[data-run-error-tag='true']")?.textContent || "").match(/(\\d+)/u)?.[1]), + selectedRunWarningTag: numberValue(String(selectedRunRow?.querySelector("[data-run-warning-tag='true']")?.textContent || "").match(/(\\d+)/u)?.[1]), }; }; const waitForRun = async (runId) => { @@ -2620,12 +2684,16 @@ const runFilterProbe = await page.evaluate(async ({ expectedRoutePrefix }) => { const observed = panelCounts(); const matchesRunDetail = observed.runId === targetRunId && observed.typeCount === expected.typeCount + && observed.errorTypeCount === expected.errorTypeCount + && observed.warningTypeCount === expected.warningTypeCount && observed.alertTypeCount === expected.alertTypeCount && observed.errorSamples === expected.errorSamples && observed.warningSamples === expected.warningSamples && observed.alertSamples === expected.alertSamples - && observed.visibleCardCount === expected.alertTypeCount - && observed.visibleAlertSamples === expected.alertSamples; + && observed.visibleRowCount === expected.alertTypeCount + && observed.visibleAlertSamples === expected.alertSamples + && observed.selectedRunErrorTag === expected.errorTypeCount + && observed.selectedRunWarningTag === expected.warningTypeCount; return { ok: panelReady === true && matchesRunDetail === true, requestedRunId, @@ -2657,6 +2725,7 @@ const ok = !navigationError && dom.chartCounts?.matchesLatestRun === true && dom.checkScope?.matchesLatestRun === true && dom.checkScope?.matchesRunDetail === true + && dom.selectedRunTags?.matchesRunDetail === true && dom.runFilterProbe?.ok === true && dom.runFilterProbe?.requestedOptionPresent === true && dom.checkScope?.belowWorkspace === true @@ -2664,6 +2733,10 @@ const ok = !navigationError && dom.scopeLabels?.latestPointLegend === true && dom.scopeLabels?.historicalSamples === true && (dom.trendDotCount === 0 || (dom.trendTooltip?.visible === true && dom.trendTooltip?.hasValues === true && dom.trendTooltip?.hasTime === true)) + && dom.trendPanelCompact?.ok === true + && dom.checkRows > 0 + && dom.checkDialog?.opened === true + && dom.checkDialog?.large === true && dom.badCardTitleCount === 0 && dom.badCardBodyCount === 0 && dom.timelineVisible === true @@ -4966,6 +5039,9 @@ function renderDashboardResult(result: Record): string { const latestRunCounts = record(dom.latestRunCounts); const latestDetailSummary = record(dom.latestDetailSummary); const checkScope = record(dom.checkScope); + const selectedRunTags = record(dom.selectedRunTags); + const trendPanelCompact = record(dom.trendPanelCompact); + const checkDialog = record(dom.checkDialog); const runFilterProbe = record(dom.runFilterProbe); const runFilterObserved = record(runFilterProbe.observed); const runFilterExpected = record(runFilterProbe.expected); @@ -4982,11 +5058,11 @@ function renderDashboardResult(result: Record): string { "", table(["NODE", "LANE", "SENTINEL", "STATUS", "URL"], [[result.node, result.lane, result.sentinelId, result.ok === true ? "pass" : "blocked", result.publicUrl]]), "", - table(["HTTP", "SHELL", "RUN_ROWS", "FINDINGS", "TABS", "ERRORS", "CONSOLE_ERR", "REQ_FAIL"], [[ + table(["HTTP", "SHELL", "RUN_ROWS", "CHECK_ROWS", "TABS", "ERRORS", "CONSOLE_ERR", "REQ_FAIL"], [[ page.httpStatus ?? "-", dom.shell, dom.runRows, - dom.findingItems, + dom.checkRows, dom.detailTabs, page.pageErrorCount, page.consoleErrorCount, @@ -4995,7 +5071,7 @@ 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", "MATCH_LATEST", "BAD_TITLE", "BAD_BODY"], [[ + table(["TREND_ERR_TYPES", "TREND_ALERT_TYPES", "TREND_TOTAL_TYPES", "TREND_EXACT", "MATCH_LATEST", "BAD_TITLE", "BAD_BODY"], [[ chartCounts.error ?? "-", chartCounts.warning ?? "-", chartCounts.total ?? "-", @@ -5005,36 +5081,46 @@ function renderDashboardResult(result: Record): string { dom.badCardBodyCount ?? "-", ]]), "", - table(["LATEST_RUN", "TYPE_COUNT", "LATEST_ERR", "LATEST_WARN", "LATEST_TOTAL", "LATEST_ALL", "HIST_ERR", "HIST_WARN"], [[ + table(["LATEST_RUN", "TYPE_COUNT", "ERR_TYPES", "ALERT_TYPES", "TOTAL_TYPES", "SAMPLE_TOTAL", "HIST_ERR", "HIST_ALERT"], [[ latestRunCounts.runId ?? "-", latestRunCounts.typeCount ?? "-", latestRunCounts.error ?? "-", latestRunCounts.warning ?? "-", latestRunCounts.total ?? "-", - latestRunCounts.allSamples ?? "-", + latestRunCounts.alertSamples ?? "-", overviewSamples.error ?? "-", overviewSamples.warning ?? "-", ]]), "", - table(["CHECK_SCOPE", "CHECK_RUN", "CHECK_TYPES", "CHECK_ALERT_TYPES", "CHECK_ERR", "CHECK_WARN", "CHECK_TOTAL", "CHECK_MATCH_LATEST", "CHECK_MATCH_DETAIL"], [[ + table(["CHECK_SCOPE", "CHECK_RUN", "CHECK_TYPES", "CHECK_ERR_TYPES", "CHECK_ALERT_TYPES", "SAMPLE_ERR", "SAMPLE_ALERT", "CHECK_MATCH_LATEST", "CHECK_MATCH_DETAIL"], [[ checkScope.scope ?? "-", checkScope.runId ?? "-", `${checkScope.typeCount ?? "-"}/${latestDetailSummary.typeCount ?? "-"}`, + `${checkScope.errorTypeCount ?? "-"}/${latestDetailSummary.errorTypeCount ?? "-"}`, `${checkScope.alertTypeCount ?? "-"}/${latestDetailSummary.alertTypeCount ?? "-"}`, checkScope.errorSamples ?? "-", - checkScope.warningSamples ?? "-", checkScope.alertSamples ?? "-", checkScope.matchesLatestRun ?? "-", checkScope.matchesRunDetail ?? "-", ]]), "", - table(["CHECK_VISIBLE", "CHECK_VISIBLE_ALERT", "BELOW_WORKSPACE", "FULL_WIDTH"], [[ - checkScope.visibleCardCount ?? "-", + table(["CHECK_VISIBLE_ROWS", "CHECK_VISIBLE_ALERT", "RUN_TAG_ERR", "RUN_TAG_ALERT", "RUN_TAG_MATCH", "BELOW_WORKSPACE", "FULL_WIDTH"], [[ + checkScope.visibleRowCount ?? "-", checkScope.visibleAlertSamples ?? "-", + selectedRunTags.error ?? "-", + selectedRunTags.warning ?? "-", + selectedRunTags.matchesRunDetail ?? "-", checkScope.belowWorkspace ?? "-", checkScope.fullWidth ?? "-", ]]), "", + table(["TREND_PANEL_SLACK", "TREND_PANEL_COMPACT", "DETAIL_DIALOG", "DIALOG_LARGE"], [[ + trendPanelCompact.bottomSlackPx ?? "-", + trendPanelCompact.ok ?? "-", + checkDialog.opened ?? "-", + checkDialog.large ?? "-", + ]]), + "", table(["WORKSPACE_H", "WORKSPACE_RATIO", "WORKSPACE_80", "CHECKS_H", "CHECKS_RATIO", "CHECKS_80", "PANES_80"], [[ `${workspaceHeight.heightPx ?? "-"}/${workspaceHeight.targetPx ?? "-"}`, workspaceHeight.ratio ?? "-", @@ -5045,13 +5131,13 @@ function renderDashboardResult(result: Record): string { panelHeights.workspacePaneBounded ?? "-", ]]), "", - table(["FILTER_RUN", "FILTER_OPTION", "FILTER_TYPES", "FILTER_ALERT_TYPES", "FILTER_ERR", "FILTER_WARN", "FILTER_TOTAL", "FILTER_MATCH_DETAIL"], [[ + table(["FILTER_RUN", "FILTER_OPTION", "FILTER_TYPES", "FILTER_ERR_TYPES", "FILTER_ALERT_TYPES", "FILTER_SAMPLE_ERR", "FILTER_SAMPLE_ALERT", "FILTER_MATCH_DETAIL"], [[ runFilterProbe.targetRunId ?? "-", runFilterProbe.requestedOptionPresent ?? "-", `${runFilterObserved.typeCount ?? "-"}/${runFilterExpected.typeCount ?? "-"}`, + `${runFilterObserved.errorTypeCount ?? "-"}/${runFilterExpected.errorTypeCount ?? "-"}`, `${runFilterObserved.alertTypeCount ?? "-"}/${runFilterExpected.alertTypeCount ?? "-"}`, runFilterObserved.errorSamples ?? "-", - runFilterObserved.warningSamples ?? "-", runFilterObserved.alertSamples ?? "-", runFilterProbe.matchesRunDetail ?? "-", ]]), diff --git a/scripts/verify-web-probe-sentinel-monitor-web.ts b/scripts/verify-web-probe-sentinel-monitor-web.ts index dc91a944..9ce2a575 100644 --- a/scripts/verify-web-probe-sentinel-monitor-web.ts +++ b/scripts/verify-web-probe-sentinel-monitor-web.ts @@ -10,6 +10,9 @@ const checks: Array<{ readonly path: string; readonly contains: readonly string[ "/api/overview", "data-monitor-trend-curve", "data-monitor-independent-scroll", + "data-check-row", + "data-check-dialog", + "错误 / 告警监测项曲线", "rootCause", ], }, @@ -19,6 +22,8 @@ const checks: Array<{ readonly path: string; readonly contains: readonly string[ contains: [ ".trend-stage", ".workspace-grid", + ".check-table", + ".check-dialog", "overflow: hidden", "overflow: auto", ".trend-red",