mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
701 lines
22 KiB
JavaScript
701 lines
22 KiB
JavaScript
import {
|
||
describeVerificationOutcome,
|
||
deriveVerificationDashboardPresentation,
|
||
} from "./harness-verification-facts.mjs";
|
||
|
||
function normalizeNumber(value) {
|
||
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||
}
|
||
|
||
function normalizeString(value, fallback = "") {
|
||
return typeof value === "string" && value.trim().length > 0
|
||
? value.trim()
|
||
: fallback;
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function formatTimestamp(value) {
|
||
const parsed = new Date(value);
|
||
if (Number.isNaN(parsed.getTime())) {
|
||
return normalizeString(value, "-");
|
||
}
|
||
return parsed.toLocaleString("zh-CN", {
|
||
hour12: false,
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
});
|
||
}
|
||
|
||
function renderStatCard(label, value, tone = "neutral") {
|
||
return `
|
||
<article class="stat-card ${tone}">
|
||
<span>${escapeHtml(label)}</span>
|
||
<strong>${escapeHtml(String(value))}</strong>
|
||
</article>
|
||
`;
|
||
}
|
||
|
||
function renderSignalList(signals) {
|
||
const items = Array.isArray(signals) ? signals.filter(Boolean) : [];
|
||
if (items.length === 0) {
|
||
return `<p class="empty">当前没有额外信号。</p>`;
|
||
}
|
||
|
||
return `
|
||
<ul class="signal-list">
|
||
${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}
|
||
</ul>
|
||
`;
|
||
}
|
||
|
||
function renderRecommendationList(recommendations) {
|
||
const items = Array.isArray(recommendations) ? recommendations : [];
|
||
if (items.length === 0) {
|
||
return `<p class="empty">当前没有新的治理建议。</p>`;
|
||
}
|
||
|
||
return items
|
||
.map(
|
||
(entry) => {
|
||
const focusVerificationFailureOutcomes = Array.isArray(
|
||
entry.focusVerificationFailureOutcomes,
|
||
)
|
||
? entry.focusVerificationFailureOutcomes
|
||
: [];
|
||
const focusVerificationRecoveredOutcomes = Array.isArray(
|
||
entry.focusVerificationRecoveredOutcomes,
|
||
)
|
||
? entry.focusVerificationRecoveredOutcomes
|
||
: [];
|
||
|
||
return `
|
||
<article class="recommendation-card">
|
||
<div class="recommendation-head">
|
||
<span class="priority">${escapeHtml(normalizeString(entry.priority, "P?"))}</span>
|
||
<h3>${escapeHtml(normalizeString(entry.title, "未命名建议"))}</h3>
|
||
</div>
|
||
<p>${escapeHtml(
|
||
Array.isArray(entry.rationale) ? entry.rationale.join(" ") : "",
|
||
)}</p>
|
||
${
|
||
focusVerificationFailureOutcomes.length > 0
|
||
? `
|
||
<p class="recommendation-meta">
|
||
<strong>关注 failure outcome:</strong>${escapeHtml(
|
||
focusVerificationFailureOutcomes.join("、"),
|
||
)}
|
||
</p>
|
||
`
|
||
: ""
|
||
}
|
||
${
|
||
focusVerificationRecoveredOutcomes.length > 0
|
||
? `
|
||
<p class="recommendation-meta">
|
||
<strong>关注 recovered outcome:</strong>${escapeHtml(
|
||
focusVerificationRecoveredOutcomes.join("、"),
|
||
)}
|
||
</p>
|
||
`
|
||
: ""
|
||
}
|
||
${
|
||
Array.isArray(entry.backlogTools) && entry.backlogTools.length > 0
|
||
? `
|
||
<div class="recommendation-subsection">
|
||
<strong>后续动作</strong>
|
||
<ul class="backlog-list">
|
||
${entry.backlogTools
|
||
.map((item) => `<li>${escapeHtml(item)}</li>`)
|
||
.join("")}
|
||
</ul>
|
||
</div>
|
||
`
|
||
: ""
|
||
}
|
||
${
|
||
Array.isArray(entry.commands) && entry.commands.length > 0
|
||
? `
|
||
<div class="recommendation-subsection">
|
||
<strong>推荐命令</strong>
|
||
<div class="command-list">
|
||
${entry.commands
|
||
.map((command) => `<code>${escapeHtml(command)}</code>`)
|
||
.join("")}
|
||
</div>
|
||
</div>
|
||
`
|
||
: ""
|
||
}
|
||
</article>
|
||
`;
|
||
},
|
||
)
|
||
.join("");
|
||
}
|
||
|
||
function deriveTrendSummary(trendReport, cleanupReport) {
|
||
const cleanupTrendSummary =
|
||
cleanupReport &&
|
||
typeof cleanupReport === "object" &&
|
||
cleanupReport.summary &&
|
||
cleanupReport.summary.trend
|
||
? cleanupReport.summary.trend
|
||
: null;
|
||
if (cleanupTrendSummary) {
|
||
return cleanupTrendSummary;
|
||
}
|
||
|
||
const latestTotals =
|
||
trendReport && typeof trendReport === "object" && trendReport.latest?.totals
|
||
? trendReport.latest.totals
|
||
: {};
|
||
const delta = trendReport && typeof trendReport === "object" ? trendReport.delta : {};
|
||
|
||
return {
|
||
sampleCount: normalizeNumber(trendReport?.sampleCount),
|
||
latestCurrentObservabilityGapCaseCount: normalizeNumber(
|
||
latestTotals.currentObservabilityGapCaseCount,
|
||
),
|
||
latestDegradedObservabilityGapCaseCount: normalizeNumber(
|
||
latestTotals.degradedObservabilityGapCaseCount,
|
||
),
|
||
currentObservabilityGapCaseDelta: normalizeNumber(
|
||
delta?.currentObservabilityGapCaseCount,
|
||
),
|
||
degradedObservabilityGapCaseDelta: normalizeNumber(
|
||
delta?.degradedObservabilityGapCaseCount,
|
||
),
|
||
};
|
||
}
|
||
|
||
function renderFocusTable(title, entries, columns) {
|
||
const rows = Array.isArray(entries) ? entries : [];
|
||
if (rows.length === 0) {
|
||
return `
|
||
<section class="panel">
|
||
<div class="section-header">
|
||
<h2>${escapeHtml(title)}</h2>
|
||
</div>
|
||
<p class="empty">当前没有数据。</p>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
return `
|
||
<section class="panel">
|
||
<div class="section-header">
|
||
<h2>${escapeHtml(title)}</h2>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
${columns.map((column) => `<th>${escapeHtml(column.label)}</th>`).join("")}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rows
|
||
.map(
|
||
(entry) => `
|
||
<tr>
|
||
${columns
|
||
.map((column) => `<td>${escapeHtml(String(column.render(entry)))}</td>`)
|
||
.join("")}
|
||
</tr>
|
||
`,
|
||
)
|
||
.join("")}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
export function renderHarnessDashboardHtml({
|
||
summaryReport,
|
||
trendReport,
|
||
cleanupReport,
|
||
title = "Lime Harness Dashboard",
|
||
}) {
|
||
const summaryTotals =
|
||
summaryReport && typeof summaryReport === "object" && summaryReport.totals
|
||
? summaryReport.totals
|
||
: {};
|
||
const trendSummary = deriveTrendSummary(trendReport, cleanupReport);
|
||
const governanceSummary =
|
||
cleanupReport &&
|
||
typeof cleanupReport === "object" &&
|
||
cleanupReport.summary &&
|
||
cleanupReport.summary.governance
|
||
? cleanupReport.summary.governance
|
||
: {};
|
||
const verificationPresentation = deriveVerificationDashboardPresentation({
|
||
summaryReport,
|
||
trendReport,
|
||
cleanupReport,
|
||
});
|
||
const verificationSummary = verificationPresentation.verificationSummary;
|
||
const currentVerificationSummary =
|
||
verificationSummary &&
|
||
typeof verificationSummary.current === "object" &&
|
||
!Array.isArray(verificationSummary.current)
|
||
? verificationSummary.current
|
||
: {};
|
||
const degradedVerificationSummary =
|
||
verificationSummary &&
|
||
typeof verificationSummary.degraded === "object" &&
|
||
!Array.isArray(verificationSummary.degraded)
|
||
? verificationSummary.degraded
|
||
: {};
|
||
const trendSignals = Array.isArray(trendReport?.signals) ? trendReport.signals : [];
|
||
const cleanupSignals = Array.isArray(cleanupReport?.signals)
|
||
? cleanupReport.signals
|
||
: [];
|
||
const recommendations = Array.isArray(cleanupReport?.recommendations)
|
||
? cleanupReport.recommendations
|
||
: [];
|
||
const sampleRows = Array.isArray(trendReport?.samples) ? trendReport.samples : [];
|
||
const verificationFocusRows = verificationPresentation.verificationFocusRows;
|
||
const currentRecoveredVerificationRows =
|
||
verificationPresentation.currentRecoveredRows;
|
||
const currentRecoveredVerificationSummary =
|
||
verificationPresentation.currentRecoveredSummaryLabel;
|
||
|
||
return `<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>${escapeHtml(title)}</title>
|
||
<style>
|
||
:root {
|
||
color-scheme: light;
|
||
--bg: #f3f6f4;
|
||
--panel: #ffffff;
|
||
--panel-muted: #f8faf8;
|
||
--border: #d8e1dc;
|
||
--text: #112118;
|
||
--muted: #5f6d64;
|
||
--accent: #0f4c5c;
|
||
--success: #177245;
|
||
--warning: #b45309;
|
||
--danger: #b42318;
|
||
--shadow: 0 16px 32px rgba(17, 33, 24, 0.08);
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
font-family:
|
||
"SF Pro Display",
|
||
"Segoe UI",
|
||
"PingFang SC",
|
||
"Hiragino Sans GB",
|
||
"Microsoft YaHei",
|
||
sans-serif;
|
||
color: var(--text);
|
||
background:
|
||
radial-gradient(circle at top left, rgba(15, 76, 92, 0.12), transparent 32%),
|
||
radial-gradient(circle at top right, rgba(23, 114, 69, 0.1), transparent 28%),
|
||
var(--bg);
|
||
}
|
||
|
||
.page {
|
||
max-width: 1440px;
|
||
margin: 0 auto;
|
||
padding: 28px 22px 42px;
|
||
}
|
||
|
||
.hero,
|
||
.panel {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 24px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.hero {
|
||
padding: 24px 26px;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.9fr);
|
||
gap: 18px;
|
||
}
|
||
|
||
.eyebrow {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 6px 12px;
|
||
border-radius: 999px;
|
||
background: #e8f7ef;
|
||
color: var(--success);
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
h1, h2, h3, p {
|
||
margin: 0;
|
||
}
|
||
|
||
h1 {
|
||
margin-top: 14px;
|
||
font-size: 32px;
|
||
line-height: 1.12;
|
||
}
|
||
|
||
.hero p {
|
||
margin-top: 12px;
|
||
color: var(--muted);
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.meta-grid,
|
||
.stat-grid,
|
||
.panel-grid {
|
||
display: grid;
|
||
gap: 14px;
|
||
}
|
||
|
||
.meta-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.meta-card,
|
||
.stat-card {
|
||
border: 1px solid var(--border);
|
||
border-radius: 18px;
|
||
background: var(--panel-muted);
|
||
padding: 14px 16px;
|
||
}
|
||
|
||
.meta-card span,
|
||
.stat-card span {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.meta-card strong,
|
||
.stat-card strong {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.stat-grid {
|
||
margin-top: 18px;
|
||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||
}
|
||
|
||
.stat-card.warning strong { color: var(--warning); }
|
||
.stat-card.danger strong { color: var(--danger); }
|
||
.stat-card.success strong { color: var(--success); }
|
||
|
||
.panel-grid {
|
||
margin-top: 20px;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.panel {
|
||
padding: 20px 22px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.section-header p {
|
||
color: var(--muted);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.signal-list {
|
||
margin: 0;
|
||
padding-left: 18px;
|
||
display: grid;
|
||
gap: 10px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.empty {
|
||
color: var(--muted);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.table-wrap {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 14px;
|
||
}
|
||
|
||
th, td {
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
text-align: left;
|
||
vertical-align: top;
|
||
}
|
||
|
||
th {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.recommendation-stack {
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.recommendation-card {
|
||
border: 1px solid var(--border);
|
||
border-radius: 18px;
|
||
padding: 16px;
|
||
background: var(--panel-muted);
|
||
}
|
||
|
||
.recommendation-head {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.priority {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 44px;
|
||
height: 28px;
|
||
padding: 0 10px;
|
||
border-radius: 999px;
|
||
background: #fff3e8;
|
||
color: var(--warning);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.recommendation-card p {
|
||
color: var(--muted);
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.recommendation-meta {
|
||
margin-top: 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.recommendation-subsection {
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.recommendation-subsection strong {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.backlog-list {
|
||
margin: 0;
|
||
padding-left: 18px;
|
||
display: grid;
|
||
gap: 8px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.command-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
code {
|
||
display: inline-flex;
|
||
padding: 8px 10px;
|
||
border-radius: 10px;
|
||
background: #102a2c;
|
||
color: #ecfeff;
|
||
font-size: 12px;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
@media (max-width: 1120px) {
|
||
.hero,
|
||
.panel-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.stat-grid {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.page {
|
||
padding: 18px 14px 30px;
|
||
}
|
||
|
||
.meta-grid,
|
||
.stat-grid {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="page">
|
||
<section class="hero">
|
||
<div>
|
||
<span class="eyebrow">Harness Engine Nightly Dashboard</span>
|
||
<h1>${escapeHtml(title)}</h1>
|
||
<p>同一份 evidence-first 主线下的 summary、trend、cleanup 视图。当前面板会明确区分主线 current 缺口与故意保留的 degraded 诊断基线,避免治理优先级被误导。</p>
|
||
<div class="stat-grid">
|
||
${renderStatCard("Ready Case", normalizeNumber(summaryTotals.readyCount), "success")}
|
||
${renderStatCard("Invalid Case", normalizeNumber(summaryTotals.invalidCount), normalizeNumber(summaryTotals.invalidCount) > 0 ? "danger" : "neutral")}
|
||
${renderStatCard("Current Gap", normalizeNumber(trendSummary.latestCurrentObservabilityGapCaseCount), normalizeNumber(trendSummary.latestCurrentObservabilityGapCaseCount) > 0 ? "danger" : "success")}
|
||
${renderStatCard("Degraded Gap", normalizeNumber(trendSummary.latestDegradedObservabilityGapCaseCount), "warning")}
|
||
${renderStatCard("Current Blocking", normalizeNumber(currentVerificationSummary.blockingFailureCaseCount), normalizeNumber(currentVerificationSummary.blockingFailureCaseCount) > 0 ? "danger" : "success")}
|
||
${renderStatCard("Current Advisory", normalizeNumber(currentVerificationSummary.advisoryFailureCaseCount), normalizeNumber(currentVerificationSummary.advisoryFailureCaseCount) > 0 ? "warning" : "neutral")}
|
||
${renderStatCard("Current Recovered", normalizeNumber(currentVerificationSummary.recoveredCaseCount), normalizeNumber(currentVerificationSummary.recoveredCaseCount) > 0 ? "success" : "neutral")}
|
||
${renderStatCard("Degraded Blocking", normalizeNumber(degradedVerificationSummary.blockingFailureCaseCount), normalizeNumber(degradedVerificationSummary.blockingFailureCaseCount) > 0 ? "warning" : "neutral")}
|
||
${renderStatCard("Recovered Outcomes", normalizeNumber(verificationSummary.recoveredCaseCount), normalizeNumber(verificationSummary.recoveredCaseCount) > 0 ? "success" : "neutral")}
|
||
${renderStatCard("Trend Samples", normalizeNumber(trendSummary.sampleCount))}
|
||
${renderStatCard("Governance Violations", normalizeNumber(governanceSummary.violationCount), normalizeNumber(governanceSummary.violationCount) > 0 ? "danger" : "neutral")}
|
||
</div>
|
||
</div>
|
||
<div class="meta-grid">
|
||
<div class="meta-card">
|
||
<span>Summary 生成时间</span>
|
||
<strong>${escapeHtml(formatTimestamp(summaryReport?.generatedAt))}</strong>
|
||
</div>
|
||
<div class="meta-card">
|
||
<span>Trend 生成时间</span>
|
||
<strong>${escapeHtml(formatTimestamp(trendReport?.generatedAt))}</strong>
|
||
</div>
|
||
<div class="meta-card">
|
||
<span>Cleanup 生成时间</span>
|
||
<strong>${escapeHtml(formatTimestamp(cleanupReport?.generatedAt))}</strong>
|
||
</div>
|
||
<div class="meta-card">
|
||
<span>当前事实源</span>
|
||
<strong>summary - history - trend - cleanup</strong>
|
||
</div>
|
||
<div class="meta-card">
|
||
<span>当前 Recovered 基线</span>
|
||
<strong>${escapeHtml(
|
||
currentRecoveredVerificationSummary ||
|
||
"当前没有额外的 recovered baseline",
|
||
)}</strong>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel-grid">
|
||
<section class="panel">
|
||
<div class="section-header">
|
||
<div>
|
||
<h2>Trend 信号</h2>
|
||
<p>只把 current gap 当主线风险,degraded gap 仅作为诊断基线保留。</p>
|
||
</div>
|
||
</div>
|
||
${renderSignalList(trendSignals)}
|
||
</section>
|
||
<section class="panel">
|
||
<div class="section-header">
|
||
<div>
|
||
<h2>Cleanup 信号</h2>
|
||
<p>nightly 治理建议与当前风险摘要。</p>
|
||
</div>
|
||
</div>
|
||
${renderSignalList(cleanupSignals)}
|
||
</section>
|
||
</section>
|
||
|
||
${renderFocusTable("Observability Gap 角色", [
|
||
{
|
||
role: "current",
|
||
latest: normalizeNumber(trendSummary.latestCurrentObservabilityGapCaseCount),
|
||
delta: normalizeNumber(trendSummary.currentObservabilityGapCaseDelta),
|
||
meaning: "主线样本里的证据缺口,必须优先治理。",
|
||
},
|
||
{
|
||
role: "degraded",
|
||
latest: normalizeNumber(trendSummary.latestDegradedObservabilityGapCaseCount),
|
||
delta: normalizeNumber(trendSummary.degradedObservabilityGapCaseDelta),
|
||
meaning: "刻意保留的诊断基线,不应直接抬高主线优先级。",
|
||
},
|
||
], [
|
||
{ label: "角色", render: (entry) => entry.role },
|
||
{ label: "latest case", render: (entry) => entry.latest },
|
||
{ label: "delta case", render: (entry) => entry.delta },
|
||
{ label: "说明", render: (entry) => entry.meaning },
|
||
])}
|
||
|
||
${renderFocusTable("Verification Outcome 焦点", verificationFocusRows, [
|
||
{ label: "Role", render: (entry) => normalizeString(entry.role, "-") },
|
||
{ label: "Signal", render: (entry) => normalizeString(entry.signal, "-") },
|
||
{ label: "Outcome", render: (entry) => normalizeString(entry.outcome, "-") },
|
||
{ label: "State", render: (entry) => normalizeString(entry.state, "-") },
|
||
{ label: "Latest Case", render: (entry) => normalizeNumber(entry?.latest?.caseCount) },
|
||
{ label: "Delta Case", render: (entry) => normalizeNumber(entry?.delta?.caseCount) },
|
||
{ label: "说明", render: (entry) => describeVerificationOutcome(entry) },
|
||
])}
|
||
|
||
${renderFocusTable("Current Recovered Baseline", currentRecoveredVerificationRows, [
|
||
{ label: "Signal", render: (entry) => normalizeString(entry.signal, "-") },
|
||
{ label: "Outcome", render: (entry) => normalizeString(entry.outcome, "-") },
|
||
{ label: "Latest Case", render: (entry) => normalizeNumber(entry?.latest?.caseCount) },
|
||
{ label: "Delta Case", render: (entry) => normalizeNumber(entry?.delta?.caseCount) },
|
||
{ label: "说明", render: (entry) => describeVerificationOutcome(entry) },
|
||
])}
|
||
|
||
${renderFocusTable("历史窗口样本", sampleRows, [
|
||
{ label: "时间", render: (entry) => formatTimestamp(entry.generatedAt) },
|
||
{ label: "来源", render: (entry) => normalizeString(entry.sourcePath, "-") },
|
||
{ label: "case", render: (entry) => normalizeNumber(entry?.totals?.caseCount) },
|
||
{ label: "ready", render: (entry) => normalizeNumber(entry?.totals?.readyCount) },
|
||
{ label: "invalid", render: (entry) => normalizeNumber(entry?.totals?.invalidCount) },
|
||
{ label: "current gap", render: (entry) => normalizeNumber(entry?.totals?.currentObservabilityGapCaseCount) },
|
||
{ label: "degraded gap", render: (entry) => normalizeNumber(entry?.totals?.degradedObservabilityGapCaseCount) },
|
||
])}
|
||
|
||
<section class="panel">
|
||
<div class="section-header">
|
||
<div>
|
||
<h2>Cleanup 建议</h2>
|
||
<p>当前 nightly 建议按已有优先级排序展示,直接复用 cleanup 报告事实源。</p>
|
||
</div>
|
||
</div>
|
||
<div class="recommendation-stack">
|
||
${renderRecommendationList(recommendations)}
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</body>
|
||
</html>`;
|
||
}
|