Add dashboard docs and richer lab UI

这个提交包含在:
hao
2026-03-17 00:37:18 -07:00
父节点 9796fa6d4c
当前提交 40ffbbd9cd
修改 74 个文件,包含 1285 行新增93 行删除

查看文件

@@ -169,6 +169,7 @@ function renderRunList() {
const active = item.run_id === state.selectedRunId ? "is-active" : "";
const title = item.advisory_meta?.title || item.advisory_id;
const reasoning = item.reasoning_lines?.[0] || item.blocked_reason || "";
const browserLabel = item.browser_evidence?.present ? "ready" : (item.browser_evidence?.required ? "required" : "n/a");
return `
<button class="run-card ${active}" data-run-id="${escapeHtml(item.run_id)}">
<div class="run-card-top">
@@ -180,7 +181,7 @@ function renderRunList() {
<div class="tag-row" style="margin-top:10px;">
<span class="tag">timeline ${escapeHtml(item.timeline?.length || 0)}</span>
<span class="tag">artifacts ${escapeHtml((item.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span>
<span class="tag">browser ${item.browser_evidence?.present ? "ready" : "missing"}</span>
<span class="tag">browser ${escapeHtml(browserLabel)}</span>
</div>
<div class="mini-muted" style="margin-top:10px;">${escapeHtml(reasoning)}</div>
</button>
@@ -234,7 +235,7 @@ function hydrateFilterOptions() {
}
function defaultArtifact(run) {
const preference = ["requests", "container", "browser", "compose", "reports"];
const preference = ["attack", "requests", "container", "browser", "baseline", "compose", "reports"];
for (const key of preference) {
const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length);
if (!group) continue;
@@ -244,6 +245,73 @@ function defaultArtifact(run) {
return null;
}
function totalProgress(progress) {
const values = Object.values(progress || {}).map((value) => Number(value || 0));
return values.reduce((sum, value) => sum + value, 0);
}
function renderProgressStrip(progress) {
const total = totalProgress(progress);
if (!total) {
return `
<div class="progress-strip">
<div class="progress-bar"><div class="progress-segment progress-other" style="width:100%"></div></div>
<div class="mini-muted">No timeline progress recorded.</div>
</div>
`;
}
const order = [
["completed", "Completed", "progress-completed"],
["blocked", "Blocked", "progress-blocked"],
["failed", "Failed", "progress-failed"],
["skipped", "Skipped", "progress-skipped"],
["planned", "Planned", "progress-planned"],
["other", "Other", "progress-other"],
];
const segments = order
.filter(([key]) => Number(progress?.[key] || 0) > 0)
.map(([key, _label, klass]) => {
const count = Number(progress?.[key] || 0);
const pct = Math.max((count / total) * 100, 4);
return `<div class="progress-segment ${klass}" style="width:${pct}%"></div>`;
})
.join("");
const legend = order
.filter(([key]) => Number(progress?.[key] || 0) > 0)
.map(([key, label, klass]) => `
<span class="tag">
<span class="swatch ${klass}"></span>
${escapeHtml(label)} ${escapeHtml(progress?.[key] || 0)}
</span>
`)
.join("");
return `
<div class="progress-strip">
<div class="progress-bar">${segments}</div>
<div class="progress-legend">${legend}</div>
</div>
`;
}
function renderStageCards(run) {
const timeline = run.timeline || [];
if (!timeline.length) {
return `<div class="empty-state">No stage records available.</div>`;
}
return `
<div class="stage-grid">
${timeline.map((item) => `
<article class="stage-card">
<strong>${escapeHtml(item.step || "-")}</strong>
<div class="${statusClass(item.status || "default")}">${escapeHtml(item.status || "unknown")}</div>
<div class="mini-muted" style="margin-top:10px;">${escapeHtml(item.detail || "-")}</div>
<div class="mini-muted" style="margin-top:8px;">${escapeHtml(item.at || "-")}</div>
</article>
`).join("")}
</div>
`;
}
async function openArtifact(href, label, kind) {
state.selectedArtifact = { href, label, kind };
document.querySelectorAll(".artifact-button").forEach((button) => {
@@ -300,6 +368,7 @@ function renderDetail() {
<span class="tag">${escapeHtml(run.repro_profile_id)}</span>
<span class="tag">${escapeHtml(run.artifact_mode)}</span>
<span class="tag">${escapeHtml(run.verification_mode)}</span>
<span class="tag">${escapeHtml(run.target_env || "local-docker")}</span>
</div>
</div>
<h2 class="detail-headline">${escapeHtml(advisory.title || run.advisory_id)}</h2>
@@ -308,11 +377,12 @@ function renderDetail() {
<a class="chip" href="${escapeHtml(run.dashboard_refs.report_html)}" target="_blank" rel="noreferrer">Open HTML report</a>
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.report_md)}" target="_blank" rel="noreferrer">Open Markdown</a>
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.bundle)}" target="_blank" rel="noreferrer">Open run JSON</a>
<a class="ghost-chip" href="./docs/frontend-dashboard-design.html" target="_blank" rel="noreferrer">Open UI spec</a>
</div>
<div class="stat-grid">
<article class="stat-card"><strong>Timeline Steps</strong><span>${escapeHtml(run.timeline?.length || 0)}</span></article>
<article class="stat-card"><strong>Artifacts</strong><span>${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span></article>
<article class="stat-card"><strong>Browser</strong><span>${run.browser_evidence?.present ? "Ready" : "Missing"}</span></article>
<article class="stat-card"><strong>Browser</strong><span>${run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional")}</span></article>
<article class="stat-card"><strong>Finished</strong><span>${escapeHtml(timeAgo(run.finished_at))}</span></article>
</div>
</section>
@@ -322,12 +392,8 @@ function renderDetail() {
<details class="glass-panel accordion" open>
<summary><span>Progress Timeline</span><span class="tag">${escapeHtml(run.timeline?.length || 0)} steps</span></summary>
<div class="accordion-content">
<div class="tag-row" style="margin-bottom:14px;">
<span class="tag">completed ${escapeHtml(run.progress?.completed || 0)}</span>
<span class="tag">blocked ${escapeHtml(run.progress?.blocked || 0)}</span>
<span class="tag">skipped ${escapeHtml(run.progress?.skipped || 0)}</span>
<span class="tag">failed ${escapeHtml(run.progress?.failed || 0)}</span>
</div>
${renderProgressStrip(run.progress)}
${renderStageCards(run)}
<div class="timeline-list">
${(run.timeline || []).map((item) => `
<article class="timeline-item">
@@ -424,7 +490,7 @@ function renderDetail() {
${(advisory.secondary_source_urls || []).map((ref) => `<a href="${escapeHtml(ref)}" target="_blank" rel="noreferrer">${escapeHtml(ref)}</a>`).join("")}
</div>
<div class="tag-row" style="margin-top:16px;">
${(advisory.secure_code_topics || []).map((topic) => `<span class="tag">${escapeHtml(topic)}</span>`).join("")}
${(advisory.secure_code_topics || []).map((topic) => `<a class="tag" href="./docs/secure-code-index.html" target="_blank" rel="noreferrer">${escapeHtml(topic)}</a>`).join("")}
</div>
</div>
</details>