fix: add web-probe screenshot and tighten sentinel dashboard
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user