From 29ab652ac6cb1db631a49878f3201d7d533ee536 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 08:25:51 +0000 Subject: [PATCH] fix: scope sentinel checks to selected run --- .../monitor-web.css | 48 ++++- .../monitor-web.js | 167 ++++++++++++++++-- scripts/src/hwlab-node-web-sentinel-cicd.ts | 94 +++++++++- 3 files changed, 288 insertions(+), 21 deletions(-) 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 9fdcedf6..32c6b62e 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.css +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.css @@ -56,7 +56,8 @@ input { .status-strip, .entry-strip, .trend-stage, -.workspace-grid { +.workspace-grid, +.checks-panel { width: min(100%, 1760px); margin: 0 auto; } @@ -546,10 +547,10 @@ select { .workspace-grid { display: grid; - grid-template-columns: minmax(260px, 0.82fr) minmax(420px, 1.34fr) minmax(300px, 1fr); + grid-template-columns: minmax(300px, 0.82fr) minmax(520px, 1.55fr); gap: 10px; flex: 0 0 auto; - min-height: clamp(680px, calc(100dvh - 220px), 980px); + min-height: clamp(520px, calc(100dvh - 360px), 760px); overflow: visible; } @@ -611,13 +612,44 @@ select { } .run-list, -.finding-list, .detail-stack { display: grid; align-content: start; gap: 8px; } +.checks-panel { + flex: 0 0 auto; + max-height: none; + min-height: clamp(440px, 62dvh, 760px); +} + +.checks-panel .filter-row select { + min-width: 150px; +} + +.checks-panel .filter-row select[aria-label="选择运行记录"] { + flex: 1 1 280px; +} + +.checks-panel .filter-row input { + flex: 1 1 260px; +} + +.check-summary { + display: grid; + grid-template-columns: repeat(6, minmax(120px, 1fr)); + gap: 8px; + margin-bottom: 10px; +} + +.finding-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); + align-content: start; + gap: 8px; +} + .run-row, .finding-card { display: grid; @@ -839,7 +871,12 @@ pre { max-height: 72dvh; } - .status-strip { + .checks-panel { + max-height: none; + } + + .status-strip, + .check-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @@ -868,6 +905,7 @@ pre { } .status-strip, + .check-summary, .detail-grid { grid-template-columns: 1fr; } 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 e3e31fc3..aac6e98b 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js @@ -1,6 +1,6 @@ // SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p12-cadence-scheduler-monitor-web. // Responsibility: Vue monitor-web runtime for sentinel trend, timeline, detail and finding observability. -import { createApp, computed, onMounted, ref } from "./vendor/vue.esm-browser.prod.js"; +import { createApp, computed, onMounted, ref, watch } from "./vendor/vue.esm-browser.prod.js"; const bootstrap = readBootstrap(); @@ -15,6 +15,10 @@ createApp({ const selectedDetail = ref(null); const runFilter = ref(""); const severityFilter = ref(""); + const checkScope = ref("run"); + const checkRunId = ref(""); + const checkTimeWindow = ref("24h"); + const checkSeverityFilter = ref("alert"); const findingFilter = ref(""); const autoRefresh = ref(true); const refreshSeconds = ref(30); @@ -76,14 +80,30 @@ createApp({ }; })); const timelineRuns = computed(() => runs.value.slice(0, 16)); - const rootCauseFindings = computed(() => { + const historicalCheckFindings = computed(() => { const rows = findings.value.filter((item) => item.checkRegistered !== false || item.rootCause || item.nextAction || ["red", "warning", "info"].includes(severityClass(item))); - return rows.slice(0, 32); + return rows.slice(0, 200); }); + const runDetailCheckFindings = computed(() => { + const rows = selectedDetail.value?.findings; + return Array.isArray(rows) ? rows : []; + }); + const scopedCheckFindings = computed(() => { + if (checkScope.value === "history") return historicalCheckFindings.value; + return runDetailCheckFindings.value; + }); + const scopedCheckSummary = computed(() => summarizeCheckRows(scopedCheckFindings.value)); const visibleCheckFindings = computed(() => { const needle = findingFilter.value.trim().toLowerCase(); - const rows = needle.length === 0 ? rootCauseFindings.value : rootCauseFindings.value.filter((item) => findingSearchText(item).includes(needle)); - return rows.slice(0, 24); + const rows = scopedCheckFindings.value.filter((item) => checkMatchesLevel(item, checkSeverityFilter.value)); + const filtered = needle.length === 0 ? rows : rows.filter((item) => findingSearchText(item).includes(needle)); + return filtered.slice(0, 64); + }); + const visibleCheckSummary = computed(() => summarizeCheckRows(visibleCheckFindings.value)); + const checkScopeRun = computed(() => runs.value.find((run) => run.id === selectedRunId.value) || selectedRun.value || latestRun.value); + const checkScopeText = computed(() => { + if (checkScope.value === "history") return `历史聚合 · ${timeWindowLabel(checkTimeWindow.value)}`; + return `运行记录 · ${checkScopeRun.value?.id || "未选择"}`; }); const cadence = computed(() => { const intervalMs = Number(overview.value?.scheduler?.intervalMs || 0); @@ -117,7 +137,7 @@ createApp({ const overviewPayload = await fetchJson("/api/overview"); const [runsResult, findingsResult] = await Promise.allSettled([ fetchJson("/api/runs?limit=30&sort=updated"), - fetchJson("/api/findings?limit=30&window=24h"), + fetchJson(findingsApiPath(checkTimeWindow.value)), ]); overview.value = overviewPayload; if (runsResult.status === "fulfilled") { @@ -151,6 +171,7 @@ createApp({ const runId = typeof run === "string" ? run : run?.id; if (!runId) return; selectedRunId.value = runId; + checkRunId.value = runId; if (!silent) selectedDetail.value = null; try { selectedDetail.value = await fetchJson(`/api/runs/${encodeURIComponent(runId)}`); @@ -163,6 +184,25 @@ createApp({ void loadAll(); } + function selectCheckRun() { + const run = runs.value.find((item) => item.id === checkRunId.value); + if (run) void selectRun(run); + } + + async function refreshHistoricalFindings() { + try { + const findingsPayload = await fetchJson(findingsApiPath(checkTimeWindow.value)); + findings.value = Array.isArray(findingsPayload.findings) ? findingsPayload.findings : Array.isArray(findingsPayload.groups) ? findingsPayload.groups : []; + } catch (cause) { + console.warn("monitor-web historical findings refresh failed", cause); + } + } + + function findingsApiPath(windowValue) { + if (windowValue === "all") return "/api/findings?limit=200"; + return `/api/findings?limit=200&window=${encodeURIComponent(windowValue || "24h")}`; + } + function currentHref(item) { if (!item || item.id === bootstrap.sentinelId) return bootstrap.basePath || "/"; if (item.monitorRoot === true) return "/"; @@ -199,6 +239,14 @@ createApp({ }, 1000); }); + watch(checkTimeWindow, () => { + if (checkScope.value === "history") void refreshHistoricalFindings(); + }); + + watch(checkScope, () => { + if (checkScope.value === "history") void refreshHistoricalFindings(); + }); + return { bootstrap, loading, @@ -210,6 +258,10 @@ createApp({ selectedDetail, runFilter, severityFilter, + checkScope, + checkRunId, + checkTimeWindow, + checkSeverityFilter, findingFilter, autoRefresh, refreshSeconds, @@ -226,12 +278,19 @@ createApp({ trendPolylines, trendDots, timelineRuns, - rootCauseFindings, + historicalCheckFindings, + runDetailCheckFindings, + scopedCheckFindings, + scopedCheckSummary, visibleCheckFindings, + visibleCheckSummary, + checkScopeRun, + checkScopeText, cadence, healthChecks, loadAll, selectRun, + selectCheckRun, refreshNow, currentHref, showTrendTooltip, @@ -254,6 +313,7 @@ createApp({ findingCode, levelLabel, findingGroupCountLabel, + timeWindowLabel, detailSummaryText, detailSummaryRows, commandSummary, @@ -501,17 +561,59 @@ createApp({
选择一条运行记录查看详情
+ - `, @@ -624,9 +725,35 @@ function alertSampleCount(item) { return redCount(item) + warningCount(item); } -function severityClass(item) { +function summarizeCheckRows(rows) { + const items = Array.isArray(rows) ? rows : []; + const errorItems = items.filter((item) => severityBucket(item) === "red"); + const warningItems = items.filter((item) => severityBucket(item) === "warning"); + const sampleCount = (entry) => Number.isFinite(Number(entry?.count)) ? Number(entry.count) : 1; + const sumSamples = (entries) => entries.reduce((sum, entry) => sum + sampleCount(entry), 0); + return { + typeCount: items.length, + errorTypeCount: errorItems.length, + warningTypeCount: warningItems.length, + alertTypeCount: errorItems.length + warningItems.length, + errorSamples: sumSamples(errorItems), + warningSamples: sumSamples(warningItems), + alertSamples: sumSamples(errorItems) + sumSamples(warningItems), + allSamples: sumSamples(items), + }; +} + +function checkMatchesLevel(item, filter) { + const value = String(filter || "alert"); + const bucket = severityBucket(item); + if (value === "all") return true; + if (value === "alert") return bucket === "red" || bucket === "warning"; + return bucket === value; +} + +function severityBucket(item) { const explicit = String(item?.maxSeverity || item?.checkLevel || item?.severity || item?.level || "").toLowerCase(); - if (["red", "critical", "error", "blocked"].includes(explicit)) return "red"; + if (["red", "critical", "error", "blocked", "failed"].includes(explicit)) return "red"; if (["warning", "warn", "amber"].includes(explicit)) return "warning"; if (["info", "notice"].includes(explicit)) return "info"; if (redCount(item) > 0) return "red"; @@ -634,6 +761,10 @@ function severityClass(item) { return "healthy"; } +function severityClass(item) { + return severityBucket(item); +} + function formatDate(value) { if (!value) return "-"; const date = new Date(value); @@ -661,6 +792,14 @@ function formatDuration(seconds) { return `${Math.round(value / 86400)}d`; } +function timeWindowLabel(value) { + if (value === "1h") return "最近 1 小时"; + if (value === "24h") return "最近 24 小时"; + if (value === "7d") return "最近 7 天"; + if (value === "all") return "全部历史"; + return String(value || "未配置"); +} + function shortId(value) { const text = String(value || ""); return text.length > 18 ? `${text.slice(0, 10)}...${text.slice(-6)}` : text || "-"; diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index 025c7b92..3c8872ad 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -2241,9 +2241,11 @@ const dom = await page.evaluate(async () => { const trendTooltip = document.querySelector("[data-monitor-trend-tooltip='true']"); const timeline = document.querySelector("[data-monitor-timeline='true']"); const workspace = document.querySelector("[data-monitor-independent-scroll='true']"); - const panes = Array.from(document.querySelectorAll(".workspace-grid .pane")); + const checksPanel = document.querySelector("[data-monitor-checks='true']"); + const panes = Array.from(document.querySelectorAll(".workspace-grid .pane, [data-monitor-checks='true']")); const detailPane = document.querySelector(".workspace-grid .pane-detail"); - const detailHeader = document.querySelector("#monitor-web-root > div > section.workspace-grid > main > div.pane-header"); + 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(), @@ -2281,6 +2283,63 @@ const dom = await page.evaluate(async () => { total: errorSampleCount(latestCounts) + warningSampleCount(latestCounts), allSamples: allSampleCount(latestCounts), }; + const latestDetailPayload = latestRunCounts.runId + ? await fetch("/api/runs/" + encodeURIComponent(latestRunCounts.runId), { cache: "no-store" }).then((item) => item.json()).catch(() => null) + : null; + const latestDetailRows = Array.isArray(latestDetailPayload?.findings) ? latestDetailPayload.findings : []; + const rowSeverity = (row) => { + const raw = String(row?.maxSeverity || row?.checkLevel || row?.severity || row?.level || "").toLowerCase(); + if (["red", "critical", "error", "blocked", "failed"].includes(raw)) return "red"; + if (["warning", "warn", "amber"].includes(raw)) return "warning"; + if (["info", "notice"].includes(raw)) return "info"; + return "healthy"; + }; + const sampleCount = (row) => Number.isFinite(Number(row?.count)) ? Number(row.count) : 1; + const summarizeRows = (rows) => { + const errorRows = rows.filter((row) => rowSeverity(row) === "red"); + const warningRows = rows.filter((row) => rowSeverity(row) === "warning"); + const sum = (items) => items.reduce((total, row) => total + sampleCount(row), 0); + return { + typeCount: rows.length, + alertTypeCount: errorRows.length + warningRows.length, + errorSamples: sum(errorRows), + warningSamples: sum(warningRows), + alertSamples: sum(errorRows) + sum(warningRows), + }; + }; + const latestDetailSummary = summarizeRows(latestDetailRows); + const workspaceRect = workspace?.getBoundingClientRect(); + const checksRect = checksPanel?.getBoundingClientRect(); + const checkScope = { + present: Boolean(checksPanel), + scope: checksPanel?.getAttribute("data-check-scope") || null, + runId: checksPanel?.getAttribute("data-check-run-id") || null, + typeCount: numberValue(checksPanel?.getAttribute("data-check-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, + 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), + }; + 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.matchesRunDetail = checkScope.present === true + && checkScope.typeCount === latestDetailSummary.typeCount + && checkScope.alertTypeCount === latestDetailSummary.alertTypeCount + && checkScope.errorSamples === latestDetailSummary.errorSamples + && checkScope.warningSamples === latestDetailSummary.warningSamples + && checkScope.alertSamples === latestDetailSummary.alertSamples + && checkScope.visibleCardCount === latestDetailSummary.alertTypeCount + && checkScope.visibleAlertSamples === latestDetailSummary.alertSamples; const overviewCounts = overviewPayload?.severityCounts && typeof overviewPayload.severityCounts === "object" && !Array.isArray(overviewPayload.severityCounts) ? overviewPayload.severityCounts : {}; @@ -2349,6 +2408,8 @@ const dom = await page.evaluate(async () => { trendPanelText: text("#trend-heading"), chartCounts, latestRunCounts, + latestDetailSummary, + checkScope, overviewSamples, scopeLabels: { latestPointLegend: legendTexts.some((item) => item.includes("最新点")), @@ -2366,6 +2427,7 @@ const dom = await page.evaluate(async () => { return style.overflowY === "auto" || style.overflowY === "scroll"; }), stickyHeader: stickyHeaderSummary(detailPane, detailHeader), + stickyChecksHeader: stickyHeaderSummary(checksPanel, checksHeader), }, layout: { viewport, @@ -2428,6 +2490,10 @@ const ok = !navigationError && dom.trendCurve === true && dom.chartCounts?.ok === true && dom.chartCounts?.matchesLatestRun === true + && dom.checkScope?.matchesLatestRun === true + && dom.checkScope?.matchesRunDetail === true + && dom.checkScope?.belowWorkspace === true + && dom.checkScope?.fullWidth === true && dom.scopeLabels?.latestPointLegend === true && dom.scopeLabels?.historicalSamples === true && (dom.trendDotCount === 0 || (dom.trendTooltip?.visible === true && dom.trendTooltip?.hasValues === true && dom.trendTooltip?.hasTime === true)) @@ -2438,6 +2504,9 @@ const ok = !navigationError && dom.scrollModel?.stickyHeader?.present === true && dom.scrollModel?.stickyHeader?.coversScroll === true && dom.scrollModel?.stickyHeader?.backgroundOpaque === true + && dom.scrollModel?.stickyChecksHeader?.present === true + && dom.scrollModel?.stickyChecksHeader?.coversScroll === true + && dom.scrollModel?.stickyChecksHeader?.backgroundOpaque === true && dom.layout?.horizontalOverflow !== true && pageErrors.length === 0; @@ -4727,6 +4796,8 @@ function renderDashboardResult(result: Record): string { const layout = record(dom.layout); const chartCounts = record(dom.chartCounts); const latestRunCounts = record(dom.latestRunCounts); + const latestDetailSummary = record(dom.latestDetailSummary); + const checkScope = record(dom.checkScope); const overviewSamples = record(dom.overviewSamples); const screenshot = record(result.screenshot); const remote = record(result.remote); @@ -4771,6 +4842,25 @@ function renderDashboardResult(result: Record): string { overviewSamples.warning ?? "-", ]]), "", + table(["CHECK_SCOPE", "CHECK_RUN", "CHECK_TYPES", "CHECK_ALERT_TYPES", "CHECK_ERR", "CHECK_WARN", "CHECK_TOTAL", "CHECK_MATCH_LATEST", "CHECK_MATCH_DETAIL"], [[ + checkScope.scope ?? "-", + checkScope.runId ?? "-", + `${checkScope.typeCount ?? "-"}/${latestDetailSummary.typeCount ?? "-"}`, + `${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 ?? "-", + checkScope.visibleAlertSamples ?? "-", + checkScope.belowWorkspace ?? "-", + checkScope.fullWidth ?? "-", + ]]), + "", table(["VIEWPORT", "DOC", "H_OVERFLOW", "OVERFLOW_COUNT"], [[ result.viewport, `${record(layout.documentSize).width ?? "-"}x${record(layout.documentSize).height ?? "-"}`,