fix: add web-probe screenshot and tighten sentinel dashboard

This commit is contained in:
Codex
2026-06-26 11:06:09 +00:00
parent 3e09444488
commit 49ce6c41be
7 changed files with 526 additions and 30 deletions
@@ -263,20 +263,20 @@ select {
}
.overview-checks {
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 8px;
margin: 0 0 14px;
}
.check-chip {
display: inline-flex;
display: flex;
align-items: center;
min-height: 30px;
max-width: 100%;
padding: 5px 10px;
border: 1px solid #d8e0ea;
border-radius: 999px;
border-radius: 8px;
background: #ffffff;
color: #475467;
font-size: 12px;
@@ -301,11 +301,12 @@ select {
}
.run-timeline {
display: flex;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
gap: 8px;
min-height: 68px;
padding: 14px 16px;
overflow-x: auto;
overflow: visible;
}
.timeline-node {
@@ -313,8 +314,9 @@ select {
grid-template-rows: 18px auto;
justify-items: center;
gap: 4px;
min-width: 76px;
max-width: 120px;
width: 100%;
min-width: 0;
max-width: none;
border: 0;
background: transparent;
color: #475467;
@@ -342,6 +344,7 @@ select {
.dashboard-grid {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr);
align-items: start;
gap: 14px;
}
@@ -462,6 +465,33 @@ select {
overflow-wrap: anywhere;
}
.run-identity {
display: grid;
gap: 5px;
min-width: 0;
}
.run-identity div {
display: grid;
grid-template-columns: 62px minmax(0, 1fr);
align-items: baseline;
gap: 8px;
}
.run-identity span {
color: var(--muted);
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
}
.run-identity code {
min-width: 0;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 12px;
overflow-wrap: anywhere;
}
.mono {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 12px;
@@ -470,10 +500,49 @@ select {
.finding-list {
display: grid;
gap: 10px;
gap: 12px;
padding: 12px;
}
.finding-group {
min-width: 0;
border: 1px solid #e3e8ef;
border-radius: 8px;
background: #ffffff;
}
.finding-group summary {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 4px 10px;
align-items: center;
min-height: 46px;
padding: 10px 12px;
cursor: pointer;
}
.finding-group summary span {
font-size: 13px;
font-weight: 800;
}
.finding-group summary strong {
font-size: 20px;
line-height: 1;
}
.finding-group summary small {
grid-column: 1 / -1;
color: var(--muted);
font-size: 12px;
}
.finding-group-list {
display: grid;
gap: 10px;
padding: 0 10px 10px;
}
.finding-aggregation {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -519,6 +588,13 @@ select {
overflow-wrap: anywhere;
}
.finding-summary {
color: #1f2937;
font-size: 13px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.finding-actions {
display: flex;
flex-wrap: wrap;
@@ -827,6 +903,10 @@ select {
.metric-card {
min-height: 96px;
}
.run-timeline {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 560px) {
@@ -862,4 +942,12 @@ select {
.findings-filter {
grid-template-columns: 1fr;
}
.run-timeline {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.finding-group:not([open]) summary {
min-height: 42px;
}
}
@@ -292,12 +292,13 @@ function renderDashboard() {
function renderOverview() {
const overview = state.overview || {};
const status = overview.status || (overview.ok ? "healthy" : "degraded");
refs.statusPill.textContent = displayStatus(status);
refs.statusPill.className = `status-pill ${statusClass(status)}`;
refs.overall.textContent = displayStatus(status);
refs.origin.textContent = overview.publicOrigin || root.dataset.publicOrigin || "-";
const latest = overview.latestRun || null;
const latestStatus = latest?.status || status;
refs.statusPill.textContent = displayStatus(latestStatus);
refs.statusPill.className = `status-pill ${statusClass(latestStatus)}`;
refs.overall.textContent = displayStatus(latestStatus);
refs.origin.textContent = `${overview.publicOrigin || root.dataset.publicOrigin || "-"} · 历史 ${displayStatus(status)}`;
refs.latestRun.textContent = latest?.runId || latest?.id || "-";
refs.latestAge.textContent = latest?.updatedAt ? `${formatRelative(latest.updatedAt)} 更新` : "-";
@@ -322,12 +323,13 @@ function renderOverview() {
}
function renderTimeline() {
refs.timelineCount.textContent = `最近 ${formatNumber(state.runs.length)}`;
const visibleRuns = state.runs.slice(0, 12);
refs.timelineCount.textContent = `最近 ${formatNumber(visibleRuns.length)} / ${formatNumber(state.runs.length)}`;
if (state.runs.length === 0) {
refs.timeline.innerHTML = '<div class="empty-state">暂无时间线</div>';
return;
}
refs.timeline.innerHTML = state.runs.slice(0, 20).map((run) => {
refs.timeline.innerHTML = visibleRuns.map((run) => {
const runId = run.runId || run.id || "-";
const title = `${displayStatus(run.status)} · ${run.findingCount ?? 0} 个发现 · ${run.updatedAt ? formatRelative(run.updatedAt) : "-"}`;
return `<button class="timeline-node ${statusClass(run.status)}" type="button" data-run-id="${escapeAttr(runId)}" title="${escapeAttr(title)}">
@@ -350,7 +352,10 @@ function renderRuns() {
const runId = run.runId || run.id || "-";
const selected = state.selectedRunId === runId ? " selected-row" : "";
return `<tr class="${selected}" data-run-id="${escapeAttr(runId)}">
<td><div class="mono">${escapeHtml(runId)}</div><small>${escapeHtml(run.observerId || "-")}</small></td>
<td><div class="run-identity">
<div><span>run</span><code>${escapeHtml(runId)}</code></div>
<div><span>observer</span><code>${escapeHtml(run.observerId || "-")}</code></div>
</div></td>
<td><span class="status-pill ${statusClass(run.status)}">${escapeHtml(displayStatus(run.status))}</span></td>
<td>${escapeHtml(run.scenarioId || "-")}</td>
<td>${escapeHtml(String(run.findingCount ?? 0))}${run.maxSeverity ? ` <span class="severity-pill ${severityClass(run.maxSeverity)}">${escapeHtml(displaySeverity(run.maxSeverity))}</span>` : ""}</td>
@@ -369,7 +374,29 @@ function renderFindings() {
refs.findingsList.innerHTML = '<div class="empty-state">暂无发现项</div>';
return;
}
refs.findingsList.innerHTML = state.findings.map((item) => {
const groups = groupedFindingsBySeverity(state.findings);
refs.findingsList.innerHTML = groups.map((group, groupIndex) => {
const open = group.key === "red" || groupIndex === 0;
const visibleItems = group.items.slice(0, group.key === "red" ? 8 : 5);
const hiddenCount = Math.max(0, group.items.length - visibleItems.length);
return `<details class="finding-group finding-group-${escapeAttr(group.key)}" ${open ? "open" : ""}>
<summary>
<span>${escapeHtml(displaySeverity(group.key))}</span>
<strong>${formatNumber(group.totalCount)}</strong>
<small>${formatNumber(group.items.length)}${hiddenCount > 0 ? ` · 其余 ${formatNumber(hiddenCount)} 组通过过滤查看` : ""}</small>
</summary>
<div class="finding-group-list">${visibleItems.map(renderFindingItem).join("")}</div>
</details>`;
}).join("");
for (const button of refs.findingsList.querySelectorAll("[data-open-finding-run]")) {
button.addEventListener("click", () => selectRun(button.dataset.openFindingRun));
}
for (const button of refs.findingsList.querySelectorAll("[data-finding-filter]")) {
button.addEventListener("click", () => applyFindingFilter(button.dataset.findingFilter, button.dataset.filterValue || ""));
}
}
function renderFindingItem(item) {
const code = item.code || item.findingId || "finding";
const latestRunId = item.latestRunId || "-";
const hasLatestRun = latestRunId !== "-";
@@ -379,23 +406,38 @@ function renderFindings() {
<span class="finding-title">${escapeHtml(codeLabel)}${codeLabel === code ? "" : ` <small class="mono">${escapeHtml(code)}</small>`}</span>
<span class="severity-pill ${severityClass(item.severity)}">${escapeHtml(displaySeverity(item.severity))}</span>
</div>
<div class="finding-summary">${escapeHtml(shortText(displayFindingSummary(code, item.summary || ""), 180))}</div>
<div class="finding-actions" aria-label="发现项过滤">
<button type="button" class="filter-chip" data-finding-filter="code" data-filter-value="${escapeAttr(code)}">${escapeHtml(code)}</button>
<button type="button" class="filter-chip" data-finding-filter="scenario" data-filter-value="${escapeAttr(item.scenarioId || "")}">${escapeHtml(item.scenarioId || "-")}</button>
</div>
<div class="finding-meta">次数=${escapeHtml(String(item.count ?? 0))} · 运行=${escapeHtml(String(item.runCount ?? 0))} · 最近=${escapeHtml(item.latestAt ? formatRelative(item.latestAt) : "-")}</div>
<div class="finding-meta mono">run=${escapeHtml(latestRunId)} report=${escapeHtml(item.latestReportJsonSha256 || "-")}</div>
<div class="finding-meta">${escapeHtml(shortText(displayFindingSummary(code, item.summary || ""), 180))}</div>
<div class="finding-meta">${escapeHtml(findingNextAction(code))}</div>
<button type="button" class="link-button" data-open-finding-run="${escapeAttr(latestRunId)}"${hasLatestRun ? "" : " disabled"}>打开最近运行</button>
</article>`;
}).join("");
for (const button of refs.findingsList.querySelectorAll("[data-open-finding-run]")) {
button.addEventListener("click", () => selectRun(button.dataset.openFindingRun));
}
for (const button of refs.findingsList.querySelectorAll("[data-finding-filter]")) {
button.addEventListener("click", () => applyFindingFilter(button.dataset.findingFilter, button.dataset.filterValue || ""));
}
function groupedFindingsBySeverity(findings) {
const severityOrder = ["red", "critical", "error", "warning", "amber", "info", "unknown"];
const groups = new Map();
for (const item of findings) {
const key = String(item.severity || "unknown").toLowerCase();
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(item);
}
return Array.from(groups.entries())
.map(([key, items]) => ({
key,
items: items.sort((a, b) => Number(b.count || 0) - Number(a.count || 0)),
totalCount: items.reduce((sum, item) => sum + Number(item.count || 0), 0),
}))
.sort((a, b) => severityRank(a.key, severityOrder) - severityRank(b.key, severityOrder));
}
function severityRank(key, order) {
const index = order.indexOf(key);
return index >= 0 ? index : order.length;
}
function renderFindingAggregation() {