fix: scope sentinel checks to selected run
This commit is contained in:
@@ -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 || "-";
|
||||
|
||||
Reference in New Issue
Block a user