fix: scope sentinel checks to selected run

This commit is contained in:
Codex
2026-06-28 08:25:51 +00:00
parent 4c7c103905
commit 29ab652ac6
3 changed files with 288 additions and 21 deletions
@@ -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;
}
@@ -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({
</div>
<div v-else class="empty">选择一条运行记录查看详情</div>
</main>
</section>
<aside class="pane pane-findings" aria-labelledby="findings-heading">
<section
class="pane pane-findings checks-panel"
aria-labelledby="findings-heading"
data-monitor-checks="true"
:data-check-scope="checkScope"
:data-check-run-id="checkScopeRun ? checkScopeRun.id : ''"
:data-check-type-count="scopedCheckSummary.typeCount"
:data-check-alert-type-count="scopedCheckSummary.alertTypeCount"
:data-check-error-samples="scopedCheckSummary.errorSamples"
:data-check-warning-samples="scopedCheckSummary.warningSamples"
:data-check-alert-samples="scopedCheckSummary.alertSamples"
:data-visible-check-count="visibleCheckFindings.length"
:data-visible-check-alert-samples="visibleCheckSummary.alertSamples"
>
<div class="pane-header">
<div>
<h2 id="findings-heading">监测项</h2>
<p class="muted">按 YAML 编号、等级和中文标题展示</p>
<p class="muted">{{ checkScopeText }}</p>
</div>
<span class="tag">{{ findings.length }}</span>
<span class="tag">错误/警告样本 {{ scopedCheckSummary.alertSamples }}</span>
</div>
<div class="filter-row">
<input v-model="findingFilter" type="search" placeholder="搜索编号 / 标题 / 根因" aria-label="搜索监测项">
<select v-model="checkScope" aria-label="监测项作用域">
<option value="run">按运行记录</option>
<option value="history">按时间聚合</option>
</select>
<select v-model="checkRunId" :disabled="checkScope === 'history'" aria-label="选择运行记录" @change="selectCheckRun">
<option v-for="run in runs" :key="run.id" :value="run.id">{{ shortId(run.id) }} · {{ formatDate(run.updatedAt || run.createdAt) }}</option>
</select>
<select v-model="checkTimeWindow" :disabled="checkScope !== 'history'" aria-label="监测项时间窗口">
<option value="1h">最近 1 小时</option>
<option value="24h">最近 24 小时</option>
<option value="7d">最近 7 天</option>
<option value="all">全部历史</option>
</select>
<select v-model="checkSeverityFilter" aria-label="监测项等级筛选">
<option value="alert">错误+警告</option>
<option value="red">错误</option>
<option value="warning">警告</option>
<option value="info">信息</option>
<option value="all">全部</option>
</select>
<input v-model="findingFilter" type="search" placeholder="搜索编号 / 标题 / 处理建议" aria-label="搜索监测项">
</div>
<div class="check-summary" aria-label="监测项摘要">
<div class="metric"><span>当前作用域</span><strong>{{ checkScope === "history" ? timeWindowLabel(checkTimeWindow) : shortId(checkScopeRun && checkScopeRun.id) }}</strong></div>
<div class="metric"><span>监测项类型</span><strong>{{ scopedCheckSummary.typeCount }}</strong></div>
<div class="metric"><span>错误/警告类型</span><strong>{{ scopedCheckSummary.alertTypeCount }}</strong></div>
<div class="metric"><span>错误样本</span><strong>{{ scopedCheckSummary.errorSamples }}</strong></div>
<div class="metric"><span>警告样本</span><strong>{{ scopedCheckSummary.warningSamples }}</strong></div>
<div class="metric"><span>错误/警告样本</span><strong>{{ scopedCheckSummary.alertSamples }}</strong></div>
</div>
<div class="finding-list">
<article
@@ -529,7 +631,6 @@ createApp({
</article>
<div v-if="visibleCheckFindings.length === 0" class="empty">暂无匹配监测项</div>
</div>
</aside>
</section>
</div>
`,
@@ -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 || "-";
+92 -2
View File
@@ -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, unknown>): 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, unknown>): 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 ?? "-"}`,