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 "空闲";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user