diff --git a/scripts/lab/dashboard_templates/legacy/assets/app.js b/scripts/lab/dashboard_templates/legacy/assets/app.js
new file mode 100644
index 00000000..7710b047
--- /dev/null
+++ b/scripts/lab/dashboard_templates/legacy/assets/app.js
@@ -0,0 +1,573 @@
+
+const state = {
+ summary: null,
+ runs: [],
+ systems: [],
+ advisories: {},
+ profiles: {},
+ selectedRunId: null,
+ selectedArtifact: null,
+ filters: { search: "", system: "", status: "", family: "" },
+ autoRefresh: true,
+ refreshMs: 5000,
+ refreshHandle: null,
+};
+
+const $ = (id) => document.getElementById(id);
+const statusClass = (status) => `status-pill ${({
+ "blocked-artifact": "status-blocked-artifact",
+ "blocked-destructive": "status-blocked-destructive",
+ "triage-manual": "status-triage-manual",
+ "verified-real": "status-verified-real",
+ "verified-synthetic": "status-verified-synthetic",
+ "suspected": "status-suspected",
+ "completed": "status-verified-real",
+ "failed": "status-blocked-artifact",
+ "skipped": "status-triage-manual"
+})[status] || "status-default"}`;
+
+function escapeHtml(value) {
+ return String(value ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
+
+function timeAgo(value) {
+ if (!value) return "-";
+ const diff = Date.now() - new Date(value).getTime();
+ if (Number.isNaN(diff)) return value;
+ const seconds = Math.floor(diff / 1000);
+ if (seconds < 60) return `${seconds}s ago`;
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `${minutes}m ago`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h ago`;
+ const days = Math.floor(hours / 24);
+ return `${days}d ago`;
+}
+
+async function fetchJson(url) {
+ const response = await fetch(`${url}?t=${Date.now()}`, { cache: "no-store" });
+ if (!response.ok) {
+ throw new Error(`${url} -> ${response.status}`);
+ }
+ return response.json();
+}
+
+async function loadData(preserveSelection = true) {
+ $("syncState").innerHTML = `Refreshing ${new Date().toLocaleTimeString()} `;
+ const previousRun = state.selectedRunId;
+ try {
+ const [summary, runs, systems, advisories, profiles] = await Promise.all([
+ fetchJson("./summary.json"),
+ fetchJson("./runs.json"),
+ fetchJson("./systems.json"),
+ fetchJson("./advisories.json"),
+ fetchJson("./profiles.json"),
+ ]);
+ state.summary = summary;
+ state.runs = runs;
+ state.systems = systems;
+ state.advisories = advisories;
+ state.profiles = profiles;
+ hydrateFilterOptions();
+
+ const hashRun = location.hash.startsWith("#run=") ? location.hash.replace("#run=", "") : null;
+ const selectedCandidate = preserveSelection ? (hashRun || previousRun) : hashRun;
+ if (selectedCandidate && runs.some((item) => item.run_id === selectedCandidate)) {
+ state.selectedRunId = selectedCandidate;
+ } else {
+ state.selectedRunId = runs[0]?.run_id || null;
+ }
+
+ renderDashboard();
+ $("syncState").innerHTML = `Live ${summary.generated_at || new Date().toISOString()} `;
+ } catch (error) {
+ $("syncState").innerHTML = `Load Failed ${escapeHtml(error.message)} `;
+ $("runList").innerHTML = `
Dashboard load failed: ${escapeHtml(error.message)}
`;
+ $("detailRoot").innerHTML = `Unable to load dashboard data. Check generated JSON and local static server state.
`;
+ }
+}
+
+function filteredRuns() {
+ return state.runs.filter((item) => {
+ if (state.filters.system && item.system_id !== state.filters.system) return false;
+ if (state.filters.status && item.verification_status !== state.filters.status) return false;
+ if (state.filters.family && item.repro_profile_id !== state.filters.family) return false;
+ if (!state.filters.search) return true;
+ const advisoryTitle = item.advisory_meta?.title || "";
+ const haystack = [item.run_id, item.advisory_id, item.system_id, item.repro_profile_id, advisoryTitle]
+ .join(" ")
+ .toLowerCase();
+ return haystack.includes(state.filters.search);
+ });
+}
+
+function renderMetrics() {
+ const metrics = [
+ { label: "Advisories", value: state.summary?.advisory_count ?? 0 },
+ { label: "Run Bundles", value: state.summary?.run_count ?? 0 },
+ ...Object.entries(state.summary?.statuses || {}).map(([label, value]) => ({ label, value })),
+ ];
+ $("metrics").innerHTML = metrics
+ .map((item) => `${escapeHtml(item.label)} ${escapeHtml(item.value)} `)
+ .join("");
+}
+
+function renderSystemCoverage() {
+ $("systemCoverage").innerHTML = state.systems
+ .map((system) => {
+ const total = Math.max(system.total || 0, 1);
+ const verified = (system.verified_real || 0) + (system.verified_synthetic || 0);
+ const fill = Math.round((verified / total) * 100);
+ return `
+
+
+ ${escapeHtml(system.display_name || system.system_id)}
+ ${escapeHtml(system.browser_present || 0)}/${escapeHtml(system.browser_required || 0)} browser
+
+
${escapeHtml(system.system_id)} · latest ${escapeHtml(system.latest_update || "-")}
+
+ real ${escapeHtml(system.verified_real || 0)}
+ synthetic ${escapeHtml(system.verified_synthetic || 0)}
+ blocked ${escapeHtml(system.blocked || 0)}
+ manual ${escapeHtml(system.manual || 0)}
+
+
+
+ `;
+ })
+ .join("");
+}
+
+function renderFailures() {
+ const failures = state.summary?.recent_failures || [];
+ $("failureFeed").innerHTML = failures.length
+ ? failures
+ .map((item) => `
+
+
+ ${escapeHtml(item.run_id)}
+ ${escapeHtml(item.status)}
+
+
${escapeHtml(item.title || item.advisory_id)}
+
${escapeHtml(item.blocked_reason || "-")}
+
+ `)
+ .join("")
+ : `No recent blockers.
`;
+}
+
+function renderRunList() {
+ const filtered = filteredRuns();
+ $("runCount").textContent = `${filtered.length} shown`;
+ $("runList").innerHTML = filtered.length
+ ? filtered
+ .map((item) => {
+ 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 `
+
+
+ ${escapeHtml(item.run_id)}
+ ${escapeHtml(item.verification_status)}
+
+ ${escapeHtml(title)}
+ ${escapeHtml(item.system_id)} · ${escapeHtml(item.repro_profile_id)} · ${escapeHtml(timeAgo(item.finished_at))}
+
+ timeline ${escapeHtml(item.timeline?.length || 0)}
+ artifacts ${escapeHtml((item.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}
+ browser ${escapeHtml(browserLabel)}
+
+ ${escapeHtml(reasoning)}
+
+ `;
+ })
+ .join("")
+ : `No runs match the current filters.
`;
+
+ document.querySelectorAll("[data-run-id]").forEach((button) => {
+ button.addEventListener("click", () => {
+ state.selectedRunId = button.dataset.runId;
+ location.hash = `run=${state.selectedRunId}`;
+ renderRunList();
+ renderDetail();
+ });
+ });
+}
+
+function renderDashboard() {
+ renderMetrics();
+ renderSystemCoverage();
+ renderFailures();
+ renderRunList();
+ renderDetail();
+}
+
+function setFilterListeners() {
+ [["searchInput", "search"], ["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => {
+ $(id).addEventListener("input", (event) => {
+ state.filters[key] = String(event.target.value || "").trim().toLowerCase();
+ if (key !== "search") {
+ state.filters[key] = String(event.target.value || "");
+ }
+ renderRunList();
+ });
+ });
+}
+
+function hydrateFilterOptions() {
+ const distinct = (items) => [...new Set(items.filter(Boolean))].sort();
+ const patchOptions = (id, values) => {
+ const control = $(id);
+ const current = control.value;
+ control.innerHTML = control.dataset.base;
+ control.innerHTML += distinct(values).map((value) => `${escapeHtml(value)} `).join("");
+ control.value = current;
+ };
+ patchOptions("systemFilter", state.runs.map((item) => item.system_id));
+ patchOptions("statusFilter", state.runs.map((item) => item.verification_status));
+ patchOptions("familyFilter", state.runs.map((item) => item.repro_profile_id));
+}
+
+function defaultArtifact(run) {
+ 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;
+ const preferredText = group.items.find((item) => item.kind === "text");
+ return preferredText || group.items[0];
+ }
+ 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 `
+
+
+
No timeline progress recorded.
+
+ `;
+ }
+ 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 `
`;
+ })
+ .join("");
+ const legend = order
+ .filter(([key]) => Number(progress?.[key] || 0) > 0)
+ .map(([key, label, klass]) => `
+
+
+ ${escapeHtml(label)} ${escapeHtml(progress?.[key] || 0)}
+
+ `)
+ .join("");
+ return `
+
+
${segments}
+
${legend}
+
+ `;
+}
+
+function renderStageCards(run) {
+ const timeline = run.timeline || [];
+ if (!timeline.length) {
+ return `No stage records available.
`;
+ }
+ return `
+
+ ${timeline.map((item) => `
+
+ ${escapeHtml(item.step || "-")}
+ ${escapeHtml(item.status || "unknown")}
+ ${escapeHtml(item.detail || "-")}
+ ${escapeHtml(item.at || "-")}
+
+ `).join("")}
+
+ `;
+}
+
+async function openArtifact(href, label, kind) {
+ state.selectedArtifact = { href, label, kind };
+ document.querySelectorAll(".artifact-button").forEach((button) => {
+ button.classList.toggle("is-active", button.dataset.href === href);
+ });
+ $("artifactLabel").textContent = label;
+ $("artifactOpen").href = href;
+ $("artifactMeta").textContent = href;
+ try {
+ if (kind === "image") {
+ $("artifactViewer").innerHTML = ` `;
+ return;
+ }
+ if (href.endsWith(".html")) {
+ $("artifactViewer").innerHTML = ``;
+ return;
+ }
+ const response = await fetch(`${href}?t=${Date.now()}`, { cache: "no-store" });
+ if (!response.ok) throw new Error(`${href} -> ${response.status}`);
+ const text = await response.text();
+ let formatted = text;
+ if (href.endsWith(".json")) {
+ try {
+ formatted = JSON.stringify(JSON.parse(text), null, 2);
+ } catch (_error) {
+ }
+ }
+ $("artifactViewer").innerHTML = `${escapeHtml(formatted)} `;
+ } catch (error) {
+ $("artifactViewer").innerHTML = `Artifact load failed: ${escapeHtml(error.message)} `;
+ }
+}
+
+function renderDetail() {
+ const run = state.runs.find((item) => item.run_id === state.selectedRunId);
+ if (!run) {
+ $("detailRoot").innerHTML = `Select a run to inspect full timeline, logs, sources, and reasoning.
`;
+ return;
+ }
+
+ const advisory = run.advisory_meta || {};
+ const profile = run.profile_meta || {};
+ const screenshotItems = (run.artifact_groups || [])
+ .find((group) => group.key === "browser")
+ ?.items.filter((item) => item.kind === "image") || [];
+
+ $("detailRoot").innerHTML = `
+
+ Local Verification Workspace
+
+
${escapeHtml(run.verification_status)}
+
+ ${escapeHtml(run.system_id)}
+ ${escapeHtml(run.repro_profile_id)}
+ ${escapeHtml(run.artifact_mode)}
+ ${escapeHtml(run.verification_mode)}
+ ${escapeHtml(run.target_env || "local-docker")}
+
+
+ ${escapeHtml(advisory.title || run.advisory_id)}
+ ${escapeHtml(advisory.summary || "No summary available.")}
+
+
+
Timeline Steps ${escapeHtml(run.timeline?.length || 0)}
+
Artifacts ${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}
+
Browser ${run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional")}
+
Finished ${escapeHtml(timeAgo(run.finished_at))}
+
+
+
+
+
+
+ Progress Timeline ${escapeHtml(run.timeline?.length || 0)} steps
+
+ ${renderProgressStrip(run.progress)}
+ ${renderStageCards(run)}
+
+ ${(run.timeline || []).map((item) => `
+
+ ${escapeHtml(item.at || "-")}
+ ${escapeHtml(item.step || "-")}
+
+
${escapeHtml(item.status || "unknown")}
+
${escapeHtml(item.detail || "-")}
+
+
+ `).join("") || `
No timeline items available.
`}
+
+
+
+
+
+ Attack Plan & Reasoning ${escapeHtml(profile.vuln_family || "unknown")}
+
+ ${run.blocked_reason ? `
Failure reason ${escapeHtml(run.blocked_reason)}
` : ""}
+
+ destructive risk ${escapeHtml(profile.destructive_risk || "-")}
+ cleanup ${escapeHtml(profile.cleanup_policy || "-")}
+ targets ${(profile.allowed_target_types || []).join(", ") || "-"}
+
+
+ ${(run.reasoning_lines || []).map((line) => `
${escapeHtml(line)}
`).join("")}
+
+
+ ${(profile.success_criteria || []).map((line) => `${escapeHtml(line)} `).join("")}
+
+
+
+
+
+ Evidence Explorer ${escapeHtml((run.artifact_groups || []).length)} groups
+
+ ${(run.artifact_groups || []).map((group) => `
+
+ ${escapeHtml(group.label)} · ${escapeHtml(group.count)}
+
+ ${group.items.map((item) => `
+
+ ${escapeHtml(item.label)}
+ ${escapeHtml(item.kind)}
+
+ `).join("")}
+
+
+ `).join("") || `
No artifacts linked for this run.
`}
+ ${screenshotItems.length ? `
+
+ ${screenshotItems.map((item) => `
+
+
+
+ ${escapeHtml(item.label)}
+
+
+ `).join("")}
+
+ ` : ""}
+
+
+
+
+ Live Log Viewer ${state.selectedArtifact ? "active" : "idle"}
+
+
+
+
Select a report, log, JSON, screenshot, or timeline file to preview it here.
+
+
+
+
+
+
+
+ Sources & Fix Topics ${escapeHtml((advisory.secondary_source_urls || []).length + (advisory.official_source_url ? 1 : 0))} links
+
+
+ ${(advisory.aliases || []).map((alias) => `${escapeHtml(alias)} `).join("")}
+
+
+
+
+
+
+
+ Run JSON raw
+ ${escapeHtml(JSON.stringify(run, null, 2))}
+
+
+
+ Advisory JSON raw
+ ${escapeHtml(JSON.stringify(advisory, null, 2))}
+
+
+
+ Profile JSON raw
+ ${escapeHtml(JSON.stringify(profile, null, 2))}
+
+
+
+ `;
+
+ document.querySelectorAll(".artifact-button").forEach((button) => {
+ button.addEventListener("click", () => openArtifact(button.dataset.href, button.dataset.label, button.dataset.kind));
+ });
+
+ $("refreshArtifact")?.addEventListener("click", () => {
+ if (state.selectedArtifact) {
+ openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind);
+ }
+ });
+
+ if (!state.selectedArtifact || !(run.artifact_groups || []).some((group) => group.items.some((item) => item.href === state.selectedArtifact.href))) {
+ const artifact = defaultArtifact(run);
+ if (artifact) {
+ openArtifact(artifact.href, artifact.label, artifact.kind);
+ }
+ } else {
+ openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind);
+ }
+}
+
+function attachGlobalActions() {
+ $("searchInput").addEventListener("input", (event) => {
+ state.filters.search = String(event.target.value || "").trim().toLowerCase();
+ renderRunList();
+ });
+ [["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => {
+ $(id).addEventListener("input", (event) => {
+ state.filters[key] = String(event.target.value || "");
+ renderRunList();
+ });
+ });
+ $("refreshDashboard").addEventListener("click", () => loadData(false));
+ $("autoRefresh").addEventListener("change", (event) => {
+ state.autoRefresh = Boolean(event.target.checked);
+ startRefreshLoop();
+ });
+}
+
+function startRefreshLoop() {
+ if (state.refreshHandle) {
+ clearInterval(state.refreshHandle);
+ state.refreshHandle = null;
+ }
+ if (!state.autoRefresh) return;
+ state.refreshHandle = setInterval(() => loadData(true), state.refreshMs);
+}
+
+async function init() {
+ ["systemFilter", "statusFilter", "familyFilter"].forEach((id) => {
+ $(id).dataset.base = $(id).innerHTML;
+ });
+ attachGlobalActions();
+ await loadData(false);
+ startRefreshLoop();
+ window.addEventListener("hashchange", () => loadData(false));
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/scripts/lab/dashboard_templates/legacy/assets/styles.css b/scripts/lab/dashboard_templates/legacy/assets/styles.css
new file mode 100644
index 00000000..61c46ff2
--- /dev/null
+++ b/scripts/lab/dashboard_templates/legacy/assets/styles.css
@@ -0,0 +1,728 @@
+
+:root {
+ --bg: #07111f;
+ --panel: rgba(9, 18, 32, 0.86);
+ --panel-2: rgba(10, 24, 44, 0.92);
+ --panel-soft: rgba(18, 32, 56, 0.74);
+ --border: rgba(137, 171, 214, 0.22);
+ --text: #f7fafc;
+ --muted: #9fb3ca;
+ --accent: #5eead4;
+ --accent-2: #ffb86b;
+ --accent-3: #90cdf4;
+ --danger: #ff7b7b;
+ --warning: #ffd166;
+ --success: #6ee7a5;
+ --shadow: 0 24px 80px rgba(1, 7, 20, 0.45);
+ --radius: 20px;
+}
+
+* { box-sizing: border-box; }
+html, body { margin: 0; min-height: 100%; }
+body {
+ font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
+ background:
+ radial-gradient(circle at top left, rgba(94, 234, 212, 0.15), transparent 28%),
+ radial-gradient(circle at top right, rgba(255, 184, 107, 0.18), transparent 22%),
+ linear-gradient(145deg, #050c16 0%, #08111f 44%, #0d1c31 100%);
+ color: var(--text);
+ overflow-x: hidden;
+}
+
+body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ background-image:
+ linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
+ background-size: 32px 32px;
+ mask-image: radial-gradient(circle at center, black 36%, transparent 78%);
+ opacity: 0.28;
+}
+
+a { color: var(--accent); text-decoration: none; }
+a:hover { text-decoration: underline; }
+button, input, select {
+ font: inherit;
+}
+
+.dashboard-shell {
+ position: relative;
+ max-width: 1640px;
+ margin: 0 auto;
+ padding: 32px 24px 40px;
+}
+
+.hero {
+ position: sticky;
+ top: 0;
+ z-index: 20;
+ backdrop-filter: blur(18px);
+ background: linear-gradient(180deg, rgba(7, 17, 31, 0.94), rgba(7, 17, 31, 0.75));
+ border: 1px solid var(--border);
+ border-radius: 28px;
+ padding: 24px 24px 20px;
+ box-shadow: var(--shadow);
+}
+
+.hero-grid {
+ display: grid;
+ grid-template-columns: 1.6fr 1fr;
+ gap: 20px;
+ align-items: start;
+}
+
+.eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--muted);
+ font-size: 0.88rem;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.eyebrow::before {
+ content: "";
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ background: radial-gradient(circle, var(--accent), rgba(94, 234, 212, 0.15));
+ box-shadow: 0 0 24px rgba(94, 234, 212, 0.8);
+ animation: pulse 2.8s ease-in-out infinite;
+}
+
+.hero h1 {
+ margin: 12px 0 10px;
+ font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif;
+ font-size: clamp(2rem, 4vw, 3.5rem);
+ line-height: 1.02;
+}
+
+.hero p {
+ margin: 0;
+ color: var(--muted);
+ max-width: 74ch;
+}
+
+.hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-top: 18px;
+}
+
+.chip, .ghost-chip {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ border-radius: 999px;
+ border: 1px solid var(--border);
+ padding: 10px 14px;
+ background: rgba(255,255,255,0.06);
+ color: var(--text);
+}
+
+.ghost-chip {
+ background: rgba(255,255,255,0.04);
+}
+
+.hero-meta {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.meta-card, .glass-panel {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+}
+
+.meta-card {
+ padding: 18px;
+ min-height: 116px;
+}
+
+.meta-card strong {
+ display: block;
+ color: var(--muted);
+ font-size: 0.84rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.meta-card span {
+ display: block;
+ margin-top: 10px;
+ font-size: 2rem;
+ font-weight: 700;
+}
+
+.workspace {
+ display: grid;
+ grid-template-columns: 420px minmax(0, 1fr);
+ gap: 20px;
+ margin-top: 22px;
+}
+
+.sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+
+.panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.panel-header h2, .panel-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--muted);
+}
+
+.glass-panel {
+ padding: 18px;
+ background:
+ linear-gradient(180deg, rgba(255,255,255,0.04), transparent 35%),
+ var(--panel);
+}
+
+.filters {
+ display: grid;
+ gap: 12px;
+}
+
+.filters label {
+ display: grid;
+ gap: 6px;
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
+.filters input, .filters select {
+ width: 100%;
+ background: rgba(255,255,255,0.05);
+ color: var(--text);
+ border: 1px solid rgba(159, 179, 202, 0.18);
+ border-radius: 14px;
+ padding: 12px 14px;
+}
+
+.run-list {
+ display: grid;
+ gap: 12px;
+ max-height: calc(100vh - 460px);
+ overflow: auto;
+ padding-right: 4px;
+}
+
+.run-card {
+ width: 100%;
+ text-align: left;
+ padding: 16px;
+ border-radius: 18px;
+ border: 1px solid rgba(159, 179, 202, 0.14);
+ background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03));
+ color: var(--text);
+ cursor: pointer;
+ transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
+}
+
+.run-card:hover, .run-card.is-active {
+ transform: translateY(-1px);
+ border-color: rgba(94, 234, 212, 0.42);
+ background: linear-gradient(180deg, rgba(94, 234, 212, 0.14), rgba(255,255,255,0.05));
+}
+
+.run-card-top, .flex-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.run-card h4 {
+ margin: 10px 0 8px;
+ font-size: 1rem;
+ line-height: 1.35;
+}
+
+.mini-muted {
+ color: var(--muted);
+ font-size: 0.86rem;
+}
+
+.status-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ border-radius: 999px;
+ padding: 6px 10px;
+ font-size: 0.82rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ border: 1px solid transparent;
+}
+
+.status-pill::before {
+ content: "";
+ width: 8px;
+ height: 8px;
+ border-radius: 999px;
+ background: currentColor;
+ box-shadow: 0 0 16px currentColor;
+}
+
+.status-blocked-artifact, .status-blocked-destructive {
+ color: var(--danger);
+ background: rgba(255, 123, 123, 0.14);
+ border-color: rgba(255, 123, 123, 0.24);
+}
+
+.status-triage-manual, .status-suspected {
+ color: var(--warning);
+ background: rgba(255, 209, 102, 0.14);
+ border-color: rgba(255, 209, 102, 0.24);
+}
+
+.status-verified-real {
+ color: var(--success);
+ background: rgba(110, 231, 165, 0.14);
+ border-color: rgba(110, 231, 165, 0.24);
+}
+
+.status-verified-synthetic {
+ color: var(--accent-3);
+ background: rgba(144, 205, 244, 0.14);
+ border-color: rgba(144, 205, 244, 0.24);
+}
+
+.status-default {
+ color: var(--accent);
+ background: rgba(94, 234, 212, 0.14);
+ border-color: rgba(94, 234, 212, 0.24);
+}
+
+.detail-view {
+ display: grid;
+ gap: 18px;
+}
+
+.detail-hero {
+ padding: 22px;
+ overflow: hidden;
+ position: relative;
+}
+
+.detail-hero::after {
+ content: "";
+ position: absolute;
+ inset: auto -20% -55% 25%;
+ height: 220px;
+ background: radial-gradient(circle, rgba(94, 234, 212, 0.2), transparent 55%);
+ pointer-events: none;
+}
+
+.detail-headline {
+ margin: 8px 0 12px;
+ font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif;
+ font-size: clamp(1.6rem, 3vw, 2.8rem);
+ line-height: 1.08;
+}
+
+.tag-row, .link-row, .artifact-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.tag {
+ display: inline-flex;
+ align-items: center;
+ padding: 7px 10px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.06);
+ border: 1px solid rgba(159, 179, 202, 0.18);
+ color: var(--text);
+ font-size: 0.86rem;
+}
+
+.stat-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 12px;
+ margin-top: 18px;
+}
+
+.stat-card {
+ padding: 14px;
+ border-radius: 16px;
+ background: rgba(255,255,255,0.04);
+ border: 1px solid rgba(159, 179, 202, 0.16);
+}
+
+.stat-card strong {
+ display: block;
+ color: var(--muted);
+ font-size: 0.78rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.stat-card span {
+ display: block;
+ margin-top: 10px;
+ font-size: 1.15rem;
+ font-weight: 700;
+}
+
+.detail-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 360px;
+ gap: 18px;
+}
+
+.stack {
+ display: grid;
+ gap: 18px;
+}
+
+.progress-strip {
+ display: grid;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.progress-bar {
+ display: flex;
+ width: 100%;
+ min-height: 12px;
+ overflow: hidden;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.08);
+ border: 1px solid rgba(159, 179, 202, 0.14);
+}
+
+.progress-segment {
+ min-width: 10px;
+ transition: width 180ms ease;
+}
+
+.progress-completed { background: linear-gradient(90deg, rgba(110, 231, 165, 0.9), rgba(94, 234, 212, 0.9)); }
+.progress-blocked { background: linear-gradient(90deg, rgba(255, 123, 123, 0.95), rgba(255, 160, 122, 0.9)); }
+.progress-failed { background: linear-gradient(90deg, rgba(255, 123, 123, 0.92), rgba(255, 209, 102, 0.88)); }
+.progress-skipped { background: linear-gradient(90deg, rgba(255,255,255,0.22), rgba(159, 179, 202, 0.3)); }
+.progress-planned { background: linear-gradient(90deg, rgba(144, 205, 244, 0.82), rgba(94, 234, 212, 0.72)); }
+.progress-other { background: linear-gradient(90deg, rgba(255,255,255,0.18), rgba(255,255,255,0.1)); }
+
+.progress-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.progress-legend .tag {
+ gap: 7px;
+}
+
+.progress-legend .swatch {
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ display: inline-block;
+}
+
+.stage-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 12px;
+ margin-bottom: 18px;
+}
+
+.stage-card {
+ padding: 14px;
+ border-radius: 16px;
+ background: rgba(255,255,255,0.04);
+ border: 1px solid rgba(159, 179, 202, 0.16);
+}
+
+.stage-card strong {
+ display: block;
+ margin-bottom: 10px;
+}
+
+.accordion {
+ overflow: hidden;
+}
+
+.accordion > summary {
+ list-style: none;
+ cursor: pointer;
+ padding: 18px 20px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.accordion > summary::-webkit-details-marker { display: none; }
+.accordion > summary span {
+ font-size: 1rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--muted);
+}
+
+.accordion .accordion-content {
+ padding: 0 20px 20px;
+ border-top: 1px solid rgba(159, 179, 202, 0.12);
+}
+
+.timeline-list {
+ display: grid;
+ gap: 12px;
+}
+
+.timeline-item {
+ display: grid;
+ grid-template-columns: 120px 180px minmax(0, 1fr);
+ gap: 12px;
+ padding: 12px 0;
+ border-bottom: 1px solid rgba(159, 179, 202, 0.12);
+}
+
+.timeline-item:last-child {
+ border-bottom: 0;
+}
+
+.timeline-step {
+ font-weight: 700;
+}
+
+.artifact-group {
+ margin-bottom: 14px;
+}
+
+.artifact-group h4 {
+ margin: 0 0 10px;
+ color: var(--muted);
+ font-size: 0.88rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.artifact-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0 10px 10px 0;
+ padding: 10px 12px;
+ border-radius: 14px;
+ border: 1px solid rgba(159, 179, 202, 0.16);
+ background: rgba(255,255,255,0.05);
+ color: var(--text);
+ cursor: pointer;
+}
+
+.artifact-button:hover, .artifact-button.is-active {
+ border-color: rgba(94, 234, 212, 0.4);
+ background: rgba(94, 234, 212, 0.12);
+}
+
+.log-viewer {
+ min-height: 420px;
+ display: grid;
+ gap: 14px;
+}
+
+.viewer-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: 10px;
+ align-items: center;
+}
+
+.viewer-frame {
+ background: rgba(2, 8, 22, 0.88);
+ border: 1px solid rgba(159, 179, 202, 0.18);
+ border-radius: 16px;
+ min-height: 300px;
+ overflow: hidden;
+}
+
+.viewer-frame pre {
+ margin: 0;
+ padding: 18px;
+ max-height: 560px;
+ overflow: auto;
+ font-family: "IBM Plex Mono", "SFMono-Regular", "Menlo", monospace;
+ font-size: 0.88rem;
+ line-height: 1.6;
+ color: #d6e5f5;
+ white-space: pre-wrap;
+}
+
+.viewer-frame img {
+ display: block;
+ width: 100%;
+ height: auto;
+}
+
+.gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 14px;
+}
+
+.gallery button {
+ all: unset;
+ cursor: pointer;
+ border-radius: 18px;
+ overflow: hidden;
+ border: 1px solid rgba(159, 179, 202, 0.18);
+ background: rgba(255,255,255,0.04);
+}
+
+.gallery img {
+ display: block;
+ width: 100%;
+ aspect-ratio: 4 / 3;
+ object-fit: cover;
+}
+
+.gallery figcaption {
+ padding: 10px 12px 14px;
+ color: var(--muted);
+ font-size: 0.84rem;
+}
+
+.failure-callout {
+ padding: 16px 18px;
+ border-radius: 18px;
+ border: 1px solid rgba(255, 123, 123, 0.2);
+ background: rgba(255, 123, 123, 0.09);
+}
+
+.json-block {
+ background: rgba(2, 8, 22, 0.72);
+ border-radius: 16px;
+ border: 1px solid rgba(159, 179, 202, 0.14);
+ padding: 16px;
+ overflow: auto;
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
+ font-size: 0.84rem;
+ line-height: 1.55;
+ color: #c9d8e8;
+}
+
+.empty-state {
+ padding: 40px 24px;
+ text-align: center;
+ color: var(--muted);
+}
+
+.failure-feed {
+ display: grid;
+ gap: 10px;
+}
+
+.failure-item {
+ padding: 12px 14px;
+ border-radius: 16px;
+ background: rgba(255,255,255,0.04);
+ border: 1px solid rgba(159, 179, 202, 0.16);
+}
+
+.system-grid {
+ display: grid;
+ gap: 10px;
+}
+
+.system-card {
+ padding: 14px 16px;
+ border-radius: 16px;
+ background: rgba(255,255,255,0.04);
+ border: 1px solid rgba(159, 179, 202, 0.14);
+}
+
+.meter {
+ position: relative;
+ height: 10px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.08);
+ overflow: hidden;
+ margin-top: 10px;
+}
+
+.meter > span {
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: var(--fill, 0%);
+ background: linear-gradient(90deg, var(--accent), var(--accent-2));
+ border-radius: inherit;
+}
+
+.sync-indicator {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.sync-indicator strong {
+ color: var(--text);
+}
+
+.dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ background: var(--accent);
+ box-shadow: 0 0 18px rgba(94, 234, 212, 0.8);
+}
+
+@keyframes pulse {
+ 0%, 100% { transform: scale(1); opacity: 0.88; }
+ 50% { transform: scale(1.35); opacity: 1; }
+}
+
+@media (max-width: 1280px) {
+ .workspace, .detail-grid, .hero-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .stat-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 760px) {
+ .dashboard-shell {
+ padding: 18px 14px 32px;
+ }
+
+ .hero {
+ position: static;
+ }
+
+ .stat-grid, .hero-meta {
+ grid-template-columns: 1fr;
+ }
+
+ .timeline-item {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/scripts/lab/dashboard_templates/legacy/index.html b/scripts/lab/dashboard_templates/legacy/index.html
new file mode 100644
index 00000000..3bbf6390
--- /dev/null
+++ b/scripts/lab/dashboard_templates/legacy/index.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+ websafe authorized lab dashboard
+
+
+
+
+
+
+
+
Authorized Lab Dashboard
+
本地攻防实证工作台
+
面向授权实验场景的本地静态前端。聚合 advisory、run bundle、日志、浏览器证据、失败原因、利用思路与源头信息,并支持可折叠细节与自动刷新。
+
+
+
+
+
+
+
+
+
+
+ Select a run to inspect full details.
+
+
+
+
+
+
diff --git a/scripts/lab/dashboard_templates/lovart/assets/icons.svg b/scripts/lab/dashboard_templates/lovart/assets/icons.svg
new file mode 100644
index 00000000..714a6d98
--- /dev/null
+++ b/scripts/lab/dashboard_templates/lovart/assets/icons.svg
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/lab/dashboard_templates/lovart/assets/styles.css b/scripts/lab/dashboard_templates/lovart/assets/styles.css
new file mode 100644
index 00000000..4788a321
--- /dev/null
+++ b/scripts/lab/dashboard_templates/lovart/assets/styles.css
@@ -0,0 +1,1069 @@
+:root {
+ --bg-dark: #0b1020;
+ --bg-deeper: #11182a;
+ --bg-card: rgba(21, 27, 40, 0.88);
+ --bg-card-strong: rgba(19, 25, 38, 0.96);
+ --bg-card-hover: rgba(34, 42, 60, 0.94);
+ --accent-blue: #4d8dff;
+ --accent-purple: #7c5cff;
+ --accent-green: #1ed49d;
+ --accent-red: #ff6b7a;
+ --accent-yellow: #ffb547;
+ --text-primary: #edf2ff;
+ --text-secondary: #95a2c2;
+ --text-dim: #7080a3;
+ --border-color: rgba(148, 163, 184, 0.18);
+ --border-strong: rgba(148, 163, 184, 0.3);
+ --shadow-lg: 0 24px 80px rgba(2, 6, 23, 0.45);
+ --shadow-md: 0 16px 40px rgba(2, 6, 23, 0.34);
+ --radius-xl: 24px;
+ --radius-lg: 18px;
+ --radius-md: 14px;
+ --radius-sm: 10px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ min-height: 100%;
+}
+
+body {
+ font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", system-ui, sans-serif;
+ color: var(--text-primary);
+ background:
+ radial-gradient(circle at 12% 18%, rgba(77, 141, 255, 0.16), transparent 26%),
+ radial-gradient(circle at 86% 22%, rgba(124, 92, 255, 0.16), transparent 24%),
+ linear-gradient(180deg, #08111f 0%, #0a1323 46%, #0d1728 100%);
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+button,
+input,
+select {
+ font: inherit;
+}
+
+.grid-bg {
+ position: fixed;
+ inset: 0;
+ background-image:
+ linear-gradient(rgba(148, 163, 184, 0.08) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(148, 163, 184, 0.08) 1px, transparent 1px);
+ background-size: 38px 38px;
+ mask-image: radial-gradient(circle at center, black 28%, transparent 82%);
+ pointer-events: none;
+ z-index: -1;
+}
+
+.dashboard-shell {
+ max-width: 1760px;
+ margin: 0 auto;
+ padding: 20px 20px 28px;
+}
+
+.icon {
+ width: 16px;
+ height: 16px;
+ flex: 0 0 auto;
+}
+
+.icon-xl {
+ width: 28px;
+ height: 28px;
+}
+
+.hero {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ padding: 24px 24px 22px;
+ border: 1px solid var(--border-color);
+ border-radius: 28px;
+ background:
+ linear-gradient(135deg, rgba(11, 16, 32, 0.96) 0%, rgba(17, 24, 42, 0.94) 48%, rgba(27, 18, 52, 0.96) 100%);
+ backdrop-filter: blur(18px);
+ box-shadow: var(--shadow-lg);
+ overflow: hidden;
+}
+
+.hero-glow {
+ position: absolute;
+ width: 420px;
+ height: 420px;
+ border-radius: 999px;
+ filter: blur(90px);
+ pointer-events: none;
+}
+
+.hero-glow-left {
+ top: -180px;
+ left: -120px;
+ background: rgba(77, 141, 255, 0.22);
+}
+
+.hero-glow-right {
+ top: -220px;
+ right: -100px;
+ background: rgba(124, 92, 255, 0.2);
+}
+
+.hero-top {
+ position: relative;
+ display: grid;
+ grid-template-columns: minmax(0, 1.25fr) minmax(360px, 0.95fr);
+ gap: 24px;
+ align-items: start;
+}
+
+.hero-eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.14em;
+ font-size: 0.78rem;
+}
+
+.hero-copy h1 {
+ margin: 14px 0 12px;
+ font-family: "IBM Plex Serif", Georgia, serif;
+ font-size: clamp(2rem, 4vw, 3.5rem);
+ line-height: 1.02;
+}
+
+.hero-copy p {
+ margin: 0;
+ max-width: 72ch;
+ color: var(--text-secondary);
+ line-height: 1.6;
+}
+
+.hero-actions {
+ position: relative;
+ display: grid;
+ gap: 14px;
+}
+
+.button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ min-height: 42px;
+ padding: 10px 14px;
+ border-radius: 12px;
+ border: 1px solid var(--border-color);
+ cursor: pointer;
+ transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
+}
+
+.button:hover {
+ transform: translateY(-1px);
+ border-color: var(--border-strong);
+}
+
+.button-primary {
+ border-color: rgba(77, 141, 255, 0.45);
+ background: linear-gradient(135deg, rgba(77, 141, 255, 0.94), rgba(90, 121, 255, 0.94));
+ color: #fff;
+ box-shadow: 0 0 24px rgba(77, 141, 255, 0.28);
+}
+
+.button-secondary {
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--text-primary);
+}
+
+.toggle-card,
+.sync-state,
+.hero-links {
+ border: 1px solid var(--border-color);
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.toggle-card {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px;
+ padding: 10px 14px;
+}
+
+.toggle-label {
+ color: var(--text-secondary);
+ font-size: 0.92rem;
+}
+
+.toggle-switch {
+ position: relative;
+ display: inline-flex;
+ width: 44px;
+ height: 24px;
+}
+
+.toggle-switch input {
+ position: absolute;
+ opacity: 0;
+ inset: 0;
+}
+
+.toggle-slider {
+ position: absolute;
+ inset: 0;
+ border-radius: 999px;
+ background: rgba(148, 163, 184, 0.18);
+ border: 1px solid rgba(148, 163, 184, 0.28);
+ transition: background 0.18s ease, border-color 0.18s ease;
+}
+
+.toggle-slider::before {
+ content: "";
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 18px;
+ height: 18px;
+ border-radius: 999px;
+ background: #fff;
+ transition: transform 0.18s ease;
+}
+
+.toggle-switch input:checked + .toggle-slider {
+ background: rgba(77, 141, 255, 0.95);
+ border-color: rgba(77, 141, 255, 0.95);
+}
+
+.toggle-switch input:checked + .toggle-slider::before {
+ transform: translateX(20px);
+}
+
+.sync-state {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 14px;
+}
+
+.sync-state strong {
+ display: block;
+ font-size: 0.92rem;
+}
+
+.sync-state span {
+ display: block;
+ margin-top: 3px;
+ color: var(--text-secondary);
+ font-size: 0.8rem;
+}
+
+.icon-sync {
+ color: var(--accent-blue);
+ width: 14px;
+ height: 14px;
+ filter: drop-shadow(0 0 10px rgba(77, 141, 255, 0.75));
+}
+
+.hero-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ padding: 12px;
+}
+
+.metrics-row {
+ position: relative;
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 16px;
+ margin-top: 22px;
+}
+
+.metric-card {
+ position: relative;
+ padding: 16px 18px;
+ border-radius: 18px;
+ border: 1px solid var(--border-color);
+ background: rgba(8, 13, 24, 0.62);
+ box-shadow: var(--shadow-md);
+ overflow: hidden;
+}
+
+.metric-card::before {
+ content: "";
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 4px;
+ background: var(--metric-color, var(--accent-purple));
+}
+
+.metric-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-size: 0.76rem;
+}
+
+.metric-value {
+ margin-top: 14px;
+ font-size: clamp(1.8rem, 3vw, 2.4rem);
+ font-weight: 700;
+}
+
+.metric-note {
+ margin-top: 8px;
+ color: var(--text-dim);
+ font-size: 0.82rem;
+}
+
+.main-container {
+ display: grid;
+ grid-template-columns: 320px minmax(0, 1fr);
+ gap: 20px;
+ margin-top: 20px;
+ min-height: calc(100vh - 260px);
+}
+
+.sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.sidebar-section,
+.panel,
+.workspace-empty {
+ border: 1px solid var(--border-color);
+ border-radius: 18px;
+ background: var(--bg-card);
+ box-shadow: var(--shadow-md);
+}
+
+.sidebar-section {
+ padding: 16px;
+}
+
+.sidebar-section-fill {
+ flex: 1 1 auto;
+ min-height: 260px;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 14px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-size: 0.76rem;
+}
+
+.section-header span:first-child {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.section-badge,
+.tag,
+.status-pill,
+.section-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 26px;
+ padding: 4px 10px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ font-size: 0.76rem;
+ white-space: nowrap;
+}
+
+.field {
+ display: grid;
+ gap: 8px;
+ color: var(--text-secondary);
+ font-size: 0.84rem;
+}
+
+.filter-group {
+ display: grid;
+ gap: 12px;
+}
+
+.search-box,
+.filter-select {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 42px;
+ padding: 0 12px;
+ border-radius: 12px;
+ border: 1px solid var(--border-color);
+ background: rgba(255, 255, 255, 0.04);
+ color: var(--text-primary);
+}
+
+.search-box input {
+ width: 100%;
+ border: 0;
+ outline: none;
+ background: transparent;
+ color: inherit;
+}
+
+.filter-select {
+ appearance: none;
+}
+
+.system-stats,
+.failure-list,
+.run-list {
+ display: grid;
+ gap: 12px;
+}
+
+.system-card,
+.failure-card,
+.run-card,
+.plan-card,
+.detail-stat,
+.artifact-group,
+.viewer-card,
+.json-card {
+ border: 1px solid var(--border-color);
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.system-card,
+.failure-card,
+.run-card,
+.plan-card,
+.detail-stat,
+.json-card {
+ padding: 12px 14px;
+}
+
+.system-title,
+.failure-title,
+.run-title {
+ font-weight: 600;
+}
+
+.system-meta,
+.failure-reason,
+.run-meta,
+.muted,
+.detail-subtitle,
+.plan-copy,
+.source-links a,
+.timeline-detail,
+.timeline-time,
+.viewer-meta,
+.footer-note,
+.empty-copy {
+ color: var(--text-secondary);
+}
+
+.meter {
+ height: 8px;
+ margin-top: 12px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.08);
+ overflow: hidden;
+}
+
+.meter > span {
+ display: block;
+ height: 100%;
+ width: var(--fill, 0%);
+ background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
+}
+
+.run-list {
+ max-height: calc(100vh - 470px);
+ overflow: auto;
+ padding-right: 4px;
+}
+
+.run-card {
+ cursor: pointer;
+ transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
+}
+
+.run-card:hover,
+.run-card.is-active {
+ transform: translateY(-1px);
+ border-color: rgba(77, 141, 255, 0.42);
+ background: rgba(77, 141, 255, 0.08);
+}
+
+.run-topline,
+.detail-topline,
+.viewer-toolbar,
+.panel-header,
+.detail-actions,
+.tag-row,
+.timeline-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.tag-row {
+ flex-wrap: wrap;
+}
+
+.status-pill {
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+}
+
+.status-pill::before {
+ content: "";
+ width: 8px;
+ height: 8px;
+ border-radius: 999px;
+ background: currentColor;
+ box-shadow: 0 0 14px currentColor;
+}
+
+.status-verified-real {
+ color: var(--accent-green);
+ border-color: rgba(30, 212, 157, 0.28);
+ background: rgba(30, 212, 157, 0.12);
+}
+
+.status-verified-synthetic {
+ color: #8fd8ff;
+ border-color: rgba(143, 216, 255, 0.28);
+ background: rgba(143, 216, 255, 0.12);
+}
+
+.status-blocked-artifact,
+.status-blocked-destructive {
+ color: var(--accent-red);
+ border-color: rgba(255, 107, 122, 0.32);
+ background: rgba(255, 107, 122, 0.12);
+}
+
+.status-triage-manual,
+.status-suspected {
+ color: var(--accent-yellow);
+ border-color: rgba(255, 181, 71, 0.28);
+ background: rgba(255, 181, 71, 0.12);
+}
+
+.status-default {
+ color: var(--accent-blue);
+ border-color: rgba(77, 141, 255, 0.28);
+ background: rgba(77, 141, 255, 0.12);
+}
+
+.workspace {
+ min-width: 0;
+}
+
+.workspace-empty {
+ display: grid;
+ place-items: center;
+ gap: 10px;
+ min-height: 320px;
+ padding: 28px;
+ text-align: center;
+}
+
+.workspace-empty h2 {
+ margin: 0;
+ font-family: "IBM Plex Serif", Georgia, serif;
+ font-size: 1.8rem;
+}
+
+.detail-hero {
+ padding: 22px 22px 20px;
+ margin-bottom: 18px;
+ border: 1px solid var(--border-color);
+ border-radius: 22px;
+ background:
+ linear-gradient(135deg, rgba(17, 24, 42, 0.98) 0%, rgba(22, 17, 44, 0.96) 100%);
+ box-shadow: var(--shadow-lg);
+}
+
+.detail-title {
+ margin: 12px 0 8px;
+ font-family: "IBM Plex Serif", Georgia, serif;
+ font-size: clamp(1.7rem, 3vw, 2.6rem);
+ line-height: 1.08;
+}
+
+.detail-actions {
+ flex-wrap: wrap;
+ margin-top: 18px;
+}
+
+.detail-stat-grid,
+.plan-grid,
+.raw-json-grid {
+ display: grid;
+ gap: 14px;
+}
+
+.detail-stat-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ margin-top: 18px;
+}
+
+.detail-stat strong,
+.plan-label {
+ display: block;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-size: 0.74rem;
+}
+
+.detail-stat span {
+ display: block;
+ margin-top: 10px;
+ font-size: 1.2rem;
+ font-weight: 700;
+}
+
+.panel {
+ overflow: hidden;
+ margin-bottom: 16px;
+}
+
+.panel-header {
+ width: 100%;
+ padding: 16px 18px;
+ background: rgba(255, 255, 255, 0.02);
+ border: 0;
+ color: inherit;
+ cursor: pointer;
+}
+
+.panel-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ font-weight: 700;
+}
+
+.panel-meta {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ color: var(--text-secondary);
+}
+
+.panel-chevron {
+ transition: transform 0.2s ease;
+}
+
+.panel.is-collapsed .panel-chevron {
+ transform: rotate(-90deg);
+}
+
+.panel-content {
+ display: grid;
+ grid-template-rows: 1fr;
+ transition: grid-template-rows 0.24s ease, opacity 0.24s ease;
+}
+
+.panel-content-inner {
+ min-height: 0;
+ overflow: hidden;
+ padding: 0 18px 18px;
+}
+
+.panel.is-collapsed .panel-content {
+ grid-template-rows: 0fr;
+ opacity: 0;
+}
+
+.timeline {
+ display: grid;
+ gap: 14px;
+}
+
+.timeline-item {
+ position: relative;
+ padding-left: 24px;
+ padding-bottom: 12px;
+ border-left: 1px solid rgba(148, 163, 184, 0.18);
+}
+
+.timeline-item:last-child {
+ padding-bottom: 0;
+}
+
+.timeline-dot {
+ position: absolute;
+ left: -7px;
+ top: 4px;
+ width: 12px;
+ height: 12px;
+ border-radius: 999px;
+ border: 2px solid currentColor;
+ background: var(--bg-dark);
+}
+
+.timeline-success {
+ color: var(--accent-green);
+}
+
+.timeline-blocked,
+.timeline-failed {
+ color: var(--accent-red);
+}
+
+.timeline-pending {
+ color: var(--accent-blue);
+}
+
+.timeline-neutral {
+ color: var(--text-dim);
+}
+
+.timeline-head strong {
+ font-size: 0.96rem;
+}
+
+.timeline-time {
+ font-size: 0.76rem;
+}
+
+.timeline-detail {
+ margin-top: 6px;
+ line-height: 1.55;
+ font-size: 0.9rem;
+}
+
+.progress-bar {
+ height: 10px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.07);
+ overflow: hidden;
+ margin-bottom: 14px;
+}
+
+.progress-segment {
+ height: 100%;
+ float: left;
+}
+
+.progress-completed {
+ background: linear-gradient(90deg, var(--accent-green), #54f0bf);
+}
+
+.progress-blocked {
+ background: linear-gradient(90deg, var(--accent-red), #ff95a0);
+}
+
+.progress-failed {
+ background: linear-gradient(90deg, #ff8a47, var(--accent-red));
+}
+
+.progress-skipped {
+ background: rgba(148, 163, 184, 0.35);
+}
+
+.progress-planned {
+ background: linear-gradient(90deg, var(--accent-blue), #8fbaff);
+}
+
+.progress-other {
+ background: rgba(148, 163, 184, 0.2);
+}
+
+.progress-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-bottom: 16px;
+}
+
+.swatch {
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+}
+
+.plan-grid,
+.raw-json-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.plan-copy {
+ margin-top: 8px;
+ line-height: 1.55;
+ font-size: 0.92rem;
+}
+
+.callout {
+ padding: 14px 16px;
+ border-radius: 14px;
+ border: 1px solid rgba(255, 107, 122, 0.28);
+ background: rgba(255, 107, 122, 0.12);
+ margin-bottom: 14px;
+}
+
+.artifact-groups {
+ display: grid;
+ gap: 16px;
+}
+
+.artifact-group h3 {
+ margin: 0 0 12px;
+ font-size: 0.86rem;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.artifact-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ gap: 12px;
+}
+
+.artifact-button {
+ display: grid;
+ gap: 8px;
+ width: 100%;
+ padding: 12px;
+ text-align: left;
+ border: 1px solid var(--border-color);
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.04);
+ color: inherit;
+ cursor: pointer;
+ transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
+}
+
+.artifact-button:hover,
+.artifact-button.is-active {
+ transform: translateY(-1px);
+ border-color: rgba(77, 141, 255, 0.42);
+ background: rgba(77, 141, 255, 0.08);
+}
+
+.artifact-kind {
+ color: var(--text-dim);
+ font-size: 0.78rem;
+}
+
+.gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 12px;
+ margin-top: 12px;
+}
+
+.gallery-card {
+ display: block;
+ width: 100%;
+ padding: 0;
+ overflow: hidden;
+}
+
+.gallery-card img {
+ display: block;
+ width: 100%;
+ aspect-ratio: 4 / 3;
+ object-fit: cover;
+}
+
+.gallery-card span {
+ display: block;
+ padding: 10px 12px 12px;
+}
+
+.viewer-card {
+ padding: 14px;
+}
+
+.viewer-toolbar {
+ flex-wrap: wrap;
+ margin-bottom: 14px;
+}
+
+.viewer-label {
+ font-size: 1rem;
+ font-weight: 600;
+}
+
+.viewer-meta {
+ margin-top: 4px;
+ font-size: 0.82rem;
+ word-break: break-all;
+}
+
+.viewer-frame {
+ min-height: 320px;
+ border: 1px solid rgba(148, 163, 184, 0.16);
+ border-radius: 14px;
+ background: rgba(5, 9, 18, 0.92);
+ overflow: hidden;
+}
+
+.viewer-frame pre {
+ margin: 0;
+ max-height: 560px;
+ padding: 16px;
+ overflow: auto;
+ color: #dbe7ff;
+ font-family: "IBM Plex Mono", "SFMono-Regular", Menlo, monospace;
+ font-size: 0.84rem;
+ line-height: 1.6;
+ white-space: pre-wrap;
+}
+
+.viewer-frame img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+
+.viewer-frame iframe {
+ display: block;
+ width: 100%;
+ min-height: 580px;
+ border: 0;
+ background: #fff;
+}
+
+.source-links {
+ display: grid;
+ gap: 10px;
+ margin-top: 14px;
+}
+
+.source-links a {
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+.json-card pre {
+ margin: 0;
+ max-height: 420px;
+ overflow: auto;
+ color: #dbe7ff;
+ font-family: "IBM Plex Mono", "SFMono-Regular", Menlo, monospace;
+ font-size: 0.82rem;
+ line-height: 1.55;
+ white-space: pre-wrap;
+}
+
+.dashboard-footer {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ align-items: center;
+ margin-top: 16px;
+ padding: 14px 6px 0;
+}
+
+.footer-note {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.84rem;
+}
+
+.footer-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ font-size: 0.84rem;
+ color: var(--text-secondary);
+}
+
+.footer-links a {
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+.empty-state {
+ padding: 24px;
+ text-align: center;
+ color: var(--text-secondary);
+}
+
+.empty-copy {
+ line-height: 1.55;
+}
+
+@media (max-width: 1320px) {
+ .hero-top,
+ .main-container,
+ .detail-stat-grid,
+ .plan-grid,
+ .raw-json-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar {
+ order: 2;
+ }
+
+ .workspace {
+ order: 1;
+ }
+}
+
+@media (max-width: 960px) {
+ .dashboard-shell {
+ padding: 14px 14px 22px;
+ }
+
+ .hero {
+ position: static;
+ padding: 18px;
+ }
+
+ .metrics-row {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .hero-links,
+ .detail-actions,
+ .tag-row,
+ .panel-meta,
+ .viewer-toolbar,
+ .dashboard-footer {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
+
+@media (max-width: 640px) {
+ .metrics-row,
+ .artifact-grid,
+ .gallery {
+ grid-template-columns: 1fr;
+ }
+
+ .hero-copy h1 {
+ font-size: 1.85rem;
+ }
+}
diff --git a/scripts/lab/dashboard_templates/lovart/index.html b/scripts/lab/dashboard_templates/lovart/index.html
new file mode 100644
index 00000000..a7d919a3
--- /dev/null
+++ b/scripts/lab/dashboard_templates/lovart/index.html
@@ -0,0 +1,168 @@
+
+
+
+
+
+ Authorized Lab Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Authorized Lab Dashboard
+
+
本地攻防实证工作台
+
+ Lovart 设计外壳已本地化并接入真实 run bundle 数据。页面只面向授权实验资产,
+ 聚合 advisory、timeline、evidence、logs、sources、raw JSON 与失败原因。
+
+
+
+
+
+
+ Refresh
+
+
+ Auto Refresh
+
+
+
+
+
+
+
+
+ Booting
+ Loading generated JSON
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select a run
+
Pick a run from the left queue to inspect timeline, evidence, logs, sources and raw JSON.
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/lab/dashboard_templates/lovart/vendor/464011bb-fbbc-4bd4-98f8-90897dd43612.html b/scripts/lab/dashboard_templates/lovart/vendor/464011bb-fbbc-4bd4-98f8-90897dd43612.html
new file mode 100644
index 00000000..d040f251
--- /dev/null
+++ b/scripts/lab/dashboard_templates/lovart/vendor/464011bb-fbbc-4bd4-98f8-90897dd43612.html
@@ -0,0 +1,1097 @@
+
+
+
+
+
+ Authorized Lab Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Runs
+ 1,248
+ 12% this week
+
+
+ Reproduction Success
+ 856
+ 98.2% Accuracy
+
+
+ Blocked / Failed
+ 42
+ Env Issues
+
+
+ Active Analysis
+ 18
+ 4 Queued
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RUN-2023-1045
+ Analysis In Progress
+
+
+
+ CVE-2023-22515
+ Severity: Critical (9.8)
+ Agent: node-alpha-01
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Docker container `atlassian/confluence-server:8.0.0` started successfully on port 8090.
+
+
+
+
+
Target responding to HTTP GET / with 200 OK. Latency 12ms.
+
+
+
+
+
Detected version 8.0.0 match. Initial check for /server-info.action accessible.
+
+
+
+
+
Sending modified XWork action request to bypass authentication middleware...
+
+
+
+
+
+
+
+
+
+
+
+
+
Strategy
+
+ The attack leverages an improperly handled parameter in the XWork action configuration. By manipulating the bootstrapStatusProvider.applicationConfig.setupComplete parameter, we can trick the application into thinking setup is incomplete.
+
+
+
+
Success Criteria
+
+ 1. HTTP 200 Response on payload delivery.
+ 2. Access to /setup/setupadministrator-start.action without auth.
+ 3. Successful creation of user 'unauthorized_admin'.
+
+
+
+
Payload Structure
+
+ GET /server-info.action?bootstrapStatusProvider.applicationConfig.setupComplete=false
+
+
+
+
+
+
+
+
+
+
+
+
+ 2023-10-27 10:42:05
+ [INFO]
+ Initializing experiment controller...
+
+
+ 2023-10-27 10:42:12
+ [INFO]
+ Pulling image atlassian/confluence-server:8.0.0
+
+
+ 2023-10-27 10:42:35
+ [INFO]
+ Container started. ID: a1b2c3d4e5f6
+
+
+ 2023-10-27 10:42:40
+ [WARN]
+ Response delay detected (1500ms). Retrying health check.
+
+
+ 2023-10-27 10:42:42
+ [INFO]
+ Target is healthy. Starting exploit chain.
+
+
+ 2023-10-27 10:42:45
+ [INFO]
+ Sending Stage 1 Payload: GET /server-info.action...
+
+
+
+
+
+
+
+
+
+
+
+
+ full_report.pdf
+
+
+
+ screenshot_01.png
+
+
+
+ http_dump.har
+
+
+
+ docker-compose.yml
+
+
+
+ db_snapshot.sql
+
+
+
+
+
+
+
+
+
+
+"run_config" : {
+ "target" : "192.168.1.105" ,
+ "port" : 8090 ,
+ "exploit_module" : "exploit/multi/http/confluence_auth_bypass" ,
+ "parameters" : {
+ "RHOSTS" : "192.168.1.105" ,
+ "RPORT" : 8090
+ }
+}
+
+
+
+
+
+
+
+
+ Broken Access Control
+ Privilege Escalation
+ Java
+ Struts2
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/lab/dashboard_templates/lovart/vendor/source-manifest.json b/scripts/lab/dashboard_templates/lovart/vendor/source-manifest.json
new file mode 100644
index 00000000..a46e5036
--- /dev/null
+++ b/scripts/lab/dashboard_templates/lovart/vendor/source-manifest.json
@@ -0,0 +1,18 @@
+{
+ "template_id": "lovart-authorized-lab-dashboard",
+ "source_url": "https://assets-persist.lovart.ai/agent_images/464011bb-fbbc-4bd4-98f8-90897dd43612.html",
+ "downloaded_at": "2026-03-17T07:56:29Z",
+ "original_filename": "464011bb-fbbc-4bd4-98f8-90897dd43612.html",
+ "vendor_source_path": "scripts/lab/dashboard_templates/lovart/vendor/464011bb-fbbc-4bd4-98f8-90897dd43612.html",
+ "runtime_template": {
+ "index": "scripts/lab/dashboard_templates/lovart/index.html",
+ "styles": "scripts/lab/dashboard_templates/lovart/assets/styles.css",
+ "app": "scripts/lab/dashboard_templates/lovart/assets/app.js",
+ "icons": "scripts/lab/dashboard_templates/lovart/assets/icons.svg"
+ },
+ "notes": [
+ "The remote Lovart HTML is tracked for provenance only and is not used at runtime.",
+ "Runtime assets are localized into repository-managed templates and generated output.",
+ "External fonts and icon CDNs are intentionally removed from the generated dashboard."
+ ]
+}