fix: align sentinel monitor checks with trend counts
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
<section class="trend-panel" aria-labelledby="trend-heading">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2 id="trend-heading">错误 / 警告样本曲线</h2>
|
||||
<p>按运行更新时间展示最近 {{ trendRows.length }} 次变化,点位为单次运行样本数</p>
|
||||
<h2 id="trend-heading">错误 / 告警监测项曲线</h2>
|
||||
<p>按运行更新时间展示最近 {{ trendRows.length }} 次变化,点位为单次运行监测项行数</p>
|
||||
</div>
|
||||
<span class="pill" :class="cadence.stale ? 'warning' : 'healthy'">{{ cadence.stale ? "非阻塞报警" : "新鲜" }}</span>
|
||||
</div>
|
||||
<div class="trend-chart-wrap">
|
||||
<svg class="trend-chart" viewBox="0 0 720 142" role="img" aria-label="错误和警告样本数量趋势" data-monitor-trend-curve="true">
|
||||
<svg class="trend-chart" viewBox="0 0 720 142" role="img" aria-label="错误和告警监测项数量趋势" data-monitor-trend-curve="true">
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="24" y2="24"></line>
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="75" y2="75"></line>
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="126" y2="126"></line>
|
||||
@@ -505,16 +578,16 @@ createApp({
|
||||
<strong>{{ shortId(hoveredTrendDot.runId) }}</strong>
|
||||
<span>{{ hoveredTrendDot.absoluteTime }}</span>
|
||||
<span>状态 {{ hoveredTrendDot.status }}</span>
|
||||
<span>错误 {{ hoveredTrendDot.red }} / 警告 {{ hoveredTrendDot.warning }} / 合计 {{ hoveredTrendDot.total }}</span>
|
||||
<span>错误 {{ hoveredTrendDot.red }} / 告警 {{ hoveredTrendDot.warning }} / 合计 {{ hoveredTrendDot.total }}</span>
|
||||
<span v-if="hoveredTrendDot.reportSha">report {{ hoveredTrendDot.reportSha }}</span>
|
||||
</div>
|
||||
<div v-if="trendRows.length === 0" class="trend-empty">暂无运行数据</div>
|
||||
</div>
|
||||
<div class="trend-legend">
|
||||
<span class="legend-item"><span class="legend-swatch red"></span>最新点错误 {{ trendErrorCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch warning"></span>最新点警告 {{ trendWarningCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch total"></span>最新点错误+警告合计 {{ trendTotalCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item">历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 警告 {{ warningCount({ severityCounts: severityTotals }) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch warning"></span>最新点告警 {{ trendWarningCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch total"></span>最新点错误+告警合计 {{ trendTotalCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item">历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 告警 {{ warningCount({ severityCounts: severityTotals }) }}</span>
|
||||
<span class="legend-item">{{ cadence.alert }}</span>
|
||||
</div>
|
||||
</section>
|
||||
@@ -538,7 +611,11 @@ createApp({
|
||||
>
|
||||
<span class="muted">{{ formatDate(run.updatedAt || run.createdAt) }}</span>
|
||||
<span class="severity-line"><span class="timeline-marker"></span><strong>{{ run.scenarioId || shortId(run.id) }}</strong></span>
|
||||
<span class="tag" :class="severityClass(run)">{{ findingCount(run) }} 项</span>
|
||||
<span class="run-alert-tags">
|
||||
<span v-if="runCheckErrorCount(run) > 0" class="tag red">错误 {{ runCheckErrorCount(run) }}</span>
|
||||
<span v-if="runCheckWarningCount(run) > 0" class="tag warning">告警 {{ runCheckWarningCount(run) }}</span>
|
||||
<span v-if="runCheckAlertCount(run) === 0" class="tag healthy">正常</span>
|
||||
</span>
|
||||
</button>
|
||||
<div v-if="timelineRuns.length === 0" class="empty">暂无时间线记录</div>
|
||||
</div>
|
||||
@@ -559,7 +636,7 @@ createApp({
|
||||
<strong>{{ redCount({ severityCounts: severityTotals }) }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>历史警告样本</span>
|
||||
<span>历史告警样本</span>
|
||||
<strong>{{ warningCount({ severityCounts: severityTotals }) }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
@@ -584,7 +661,7 @@ createApp({
|
||||
<select v-model="severityFilter" aria-label="严重级别筛选">
|
||||
<option value="">全部</option>
|
||||
<option value="red">错误</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="warning">告警</option>
|
||||
<option value="info">信息</option>
|
||||
<option value="healthy">正常</option>
|
||||
</select>
|
||||
@@ -600,7 +677,11 @@ createApp({
|
||||
>
|
||||
<span class="row-line">
|
||||
<span class="severity-line"><span class="severity-dot"></span><strong>{{ run.scenarioId || shortId(run.id) }}</strong></span>
|
||||
<span class="tag" :class="severityClass(run)">{{ findingCount(run) }}</span>
|
||||
<span class="run-alert-tags">
|
||||
<span v-if="runCheckErrorCount(run) > 0" class="tag red" data-run-error-tag="true">错误 {{ runCheckErrorCount(run) }}</span>
|
||||
<span v-if="runCheckWarningCount(run) > 0" class="tag warning" data-run-warning-tag="true">告警 {{ runCheckWarningCount(run) }}</span>
|
||||
<span v-if="runCheckAlertCount(run) === 0" class="tag healthy">正常</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="row-line muted">
|
||||
<span>{{ run.status || "-" }}</span>
|
||||
@@ -625,7 +706,7 @@ createApp({
|
||||
<div class="detail-grid">
|
||||
<div class="metric"><span>状态</span><strong>{{ selectedRun.status || "-" }}</strong></div>
|
||||
<div class="metric"><span>监测项类型</span><strong>{{ findingCount(selectedRun) }}</strong></div>
|
||||
<div class="metric"><span>错误/警告样本</span><strong>{{ alertSampleCount(selectedRun) }}</strong></div>
|
||||
<div class="metric"><span>错误/告警样本</span><strong>{{ alertSampleCount(selectedRun) }}</strong></div>
|
||||
<div class="metric"><span>全部样本</span><strong>{{ findingSampleCount(selectedRun) }}</strong></div>
|
||||
<div class="metric"><span>Observer</span><strong>{{ selectedRun.observerId || "-" }}</strong></div>
|
||||
<div class="metric"><span>更新时间</span><strong>{{ formatDate(selectedRun.updatedAt || selectedRun.createdAt) }}</strong></div>
|
||||
@@ -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({
|
||||
<h2 id="findings-heading">监测项</h2>
|
||||
<p class="muted">{{ checkScopeText }}</p>
|
||||
</div>
|
||||
<span class="tag">错误/警告样本 {{ scopedCheckSummary.alertSamples }}</span>
|
||||
<span class="tag">错误/告警样本 {{ scopedCheckSummary.alertSamples }}</span>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<select v-model="checkScope" aria-label="监测项作用域">
|
||||
@@ -692,9 +775,9 @@ createApp({
|
||||
<option value="all">全部历史</option>
|
||||
</select>
|
||||
<select v-model="checkSeverityFilter" aria-label="监测项等级筛选">
|
||||
<option value="alert">错误+警告</option>
|
||||
<option value="alert">错误+告警</option>
|
||||
<option value="red">错误</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="warning">告警</option>
|
||||
<option value="info">信息</option>
|
||||
<option value="all">全部</option>
|
||||
</select>
|
||||
@@ -703,28 +786,101 @@ createApp({
|
||||
<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.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 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
|
||||
v-for="item in visibleCheckFindings"
|
||||
:key="item.code || item.findingId || item.latestRunId"
|
||||
class="finding-card"
|
||||
:class="severityClass(item)"
|
||||
>
|
||||
<span class="row-line">
|
||||
<span class="severity-line"><span class="severity-dot"></span><span class="check-code">{{ findingCode(item) }}</span><strong>{{ findingTitle(item) }}</strong></span>
|
||||
<span class="tag" :class="severityClass(item)">{{ levelLabel(item) }} · {{ findingGroupCountLabel(item) }}</span>
|
||||
</span>
|
||||
<p class="muted">{{ rootCauseText(item) }}</p>
|
||||
<p v-if="item.nextAction" class="muted">处理: {{ item.nextAction }}</p>
|
||||
</article>
|
||||
<div class="finding-list check-table-wrap">
|
||||
<table v-if="visibleCheckFindings.length > 0" class="check-table" aria-label="监测项列表">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">编号</th>
|
||||
<th scope="col">等级</th>
|
||||
<th scope="col">标题</th>
|
||||
<th scope="col">样本</th>
|
||||
<th scope="col">运行记录</th>
|
||||
<th scope="col">时间</th>
|
||||
<th scope="col">处理建议</th>
|
||||
<th scope="col">详情</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in visibleCheckFindings"
|
||||
:key="checkRowKey(item, index)"
|
||||
class="check-row"
|
||||
:class="severityClass(item)"
|
||||
data-check-row="true"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:aria-label="'查看监测项详情 ' + findingCode(item) + ' ' + findingTitle(item)"
|
||||
:data-check-code="findingCode(item)"
|
||||
@click="openCheckDetail(item)"
|
||||
@keydown.enter.prevent="openCheckDetail(item)"
|
||||
@keydown.space.prevent="openCheckDetail(item)"
|
||||
>
|
||||
<td><span class="check-code">{{ findingCode(item) }}</span></td>
|
||||
<td><span class="tag" :class="severityClass(item)">{{ levelLabel(item) }}</span></td>
|
||||
<td class="check-title-cell"><strong>{{ findingTitle(item) }}</strong><span>{{ rootCauseText(item) }}</span></td>
|
||||
<td>{{ findingGroupCountLabel(item) }}</td>
|
||||
<td class="mono">{{ checkRunText(item) }}</td>
|
||||
<td>{{ checkTimeText(item) }}</td>
|
||||
<td>{{ checkActionText(item) }}</td>
|
||||
<td><span class="detail-link">查看</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="visibleCheckFindings.length === 0" class="empty">暂无匹配监测项</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="activeCheckItem"
|
||||
class="check-dialog-backdrop"
|
||||
data-check-dialog="true"
|
||||
@click.self="closeCheckDetail"
|
||||
@keydown.esc="closeCheckDetail"
|
||||
>
|
||||
<section class="check-dialog" role="dialog" aria-modal="true" aria-labelledby="check-dialog-title" tabindex="-1">
|
||||
<header class="check-dialog-header">
|
||||
<div>
|
||||
<span class="check-code">{{ findingCode(activeCheckItem) }}</span>
|
||||
<h2 id="check-dialog-title">{{ findingTitle(activeCheckItem) }}</h2>
|
||||
<p class="muted">{{ checkScopeText }}</p>
|
||||
</div>
|
||||
<div class="check-dialog-actions">
|
||||
<span class="tag" :class="severityClass(activeCheckItem)">{{ levelLabel(activeCheckItem) }} · {{ findingGroupCountLabel(activeCheckItem) }}</span>
|
||||
<button class="dialog-close" type="button" aria-label="关闭监测项详情" @click="closeCheckDetail">关闭</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="check-dialog-body">
|
||||
<section class="detail-card">
|
||||
<h3>基本信息</h3>
|
||||
<div class="summary-list">
|
||||
<div v-for="row in checkDetailRows(activeCheckItem)" :key="row.key" class="summary-row">
|
||||
<span>{{ row.label }}</span>
|
||||
<strong>{{ row.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="detail-card">
|
||||
<h3>用户影响</h3>
|
||||
<p>{{ rootCauseText(activeCheckItem) }}</p>
|
||||
<p class="muted">处理建议: {{ checkActionText(activeCheckItem) }}</p>
|
||||
</section>
|
||||
<section class="detail-card detail-card-wide">
|
||||
<h3>关联证据</h3>
|
||||
<div class="summary-list">
|
||||
<div v-for="row in checkEvidenceRows(activeCheckItem)" :key="row.key" class="summary-row">
|
||||
<span>{{ row.label }}</span>
|
||||
<strong>{{ row.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}).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 "空闲";
|
||||
}
|
||||
|
||||
@@ -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, unknown>): 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, unknown>): 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, unknown>): string {
|
||||
"",
|
||||
table(["TITLE", "STATUS_TEXT", "CONTRACT", "BASE_PATH"], [[dom.title, dom.statusText, dataset.contractVersion, dataset.basePath ?? "-"]]),
|
||||
"",
|
||||
table(["TREND_ERROR", "TREND_WARNING", "TREND_TOTAL", "TREND_EXACT", "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, unknown>): 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, unknown>): 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 ?? "-",
|
||||
]]),
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user