Add dashboard docs and richer lab UI
这个提交包含在:
@@ -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>
|
||||
|
||||
在新工单中引用
屏蔽一个用户