diff --git a/scripts/lab/dashboard_templates/lovart/assets/app.js b/scripts/lab/dashboard_templates/lovart/assets/app.js
index f7c575b2..c8676c00 100644
--- a/scripts/lab/dashboard_templates/lovart/assets/app.js
+++ b/scripts/lab/dashboard_templates/lovart/assets/app.js
@@ -63,6 +63,10 @@ const DATA_HUB_ITEMS = [
{ title: "entity-completeness.json", href: "/data/entity-completeness.json", description: "实体级 catalog 完整度、版本映射和 workflow 覆盖。", badge: "json" },
{ title: "entity-discovery-backlog.json", href: "/data/entity-discovery-backlog.json", description: "发现但尚未正式编目的 repo / 插件 / 包 backlog。", badge: "json" },
{ title: "entity-queues.json", href: "/data/entity-queues.json", description: "discovery/history/latest/workflow 四类队列摘要。", badge: "json" },
+ { title: "version-completeness.json", href: "/data/version-completeness.json", description: "最新版本同步覆盖、安全相关版本历史与 auto-promoted 统计。", badge: "json" },
+ { title: "version-backlog.json", href: "/data/version-backlog.json", description: "source-gap、未解决版本缺口与 lab pending 队列。", badge: "json" },
+ { title: "release-index.json", href: "/data/release-index.json", description: "安全相关版本记录索引真值。", badge: "json" },
+ { title: "lab-enqueue-summary.json", href: "/data/lab-enqueue-summary.json", description: "版本变化触发的 lab 入队与 pending 摘要。", badge: "json" },
{ title: "runs.json", href: "/runs.json", description: "最近运行的结构化详情,可用于 UI 和调试。", badge: "json" },
{ title: "systems.json", href: "/systems.json", description: "系统级覆盖、分类、更新时间和浏览器证据统计。", badge: "json" },
{ title: "entities.json", href: "/entities.json", description: "分层实体索引、实体状态和系统归属。", badge: "json" },
@@ -101,6 +105,7 @@ const state = {
architecture: null,
completeness: null,
entityCompleteness: null,
+ versionCompleteness: null,
sourceHealth: null,
alerts: [],
monitorSummary: null,
@@ -297,11 +302,14 @@ function familyOptions() {
function metricCards() {
const completeness = state.completeness || state.summary?.completeness || {};
const entityCoverage = state.entityCompleteness || state.summary?.entity_coverage || completeness.entity_coverage || {};
+ const versionCoverage = state.versionCompleteness || state.summary?.version_coverage || completeness.version_coverage || {};
const monitoring = state.monitorSummary || state.summary?.monitoring || {};
const advisoryTotal = Number(completeness.advisory_total || state.summary?.advisory_count || 0);
const advisorySuccess = Number(completeness.verified_real || 0);
const catalogedEntities = Number(entityCoverage.cataloged_entity_total || 0);
const candidateEntities = Number(entityCoverage.candidate_entity_total || 0);
+ const latestVersionSynced = Number(versionCoverage.latest_version_synced_count || 0);
+ const sourceGapCount = Number(versionCoverage.source_gap_count || 0);
const activeSources = Number(monitoring.active_source_count || state.sourceHealth?.active_source_count || 0);
const greenSources = Number(monitoring.green_source_count || state.sourceHealth?.green_source_count || 0);
const openAlerts = Number(monitoring.open_alert_count || state.sourceHealth?.open_alert_count || 0);
@@ -322,6 +330,13 @@ function metricCards() {
color: "var(--accent-yellow)",
iconName: "systems"
},
+ {
+ label: "版本同步",
+ value: latestVersionSynced,
+ note: `source-gap ${sourceGapCount}`,
+ color: "var(--accent-blue)",
+ iconName: "spark"
+ },
{
label: "active sources",
value: activeSources,
@@ -602,6 +617,7 @@ function renderSystemCards(items, compact = false) {
const verified = Number(system.verified_real || 0) + Number(system.verified_synthetic || 0);
const coverage = Math.round((verified / total) * 100);
const entitySummary = system.entity_summary || {};
+ const versionSummary = system.version_summary || {};
const topEntities = system.top_entities || [];
const backlogPreview = system.backlog_preview || [];
return `
@@ -617,6 +633,8 @@ function renderSystemCards(items, compact = false) {
阻塞 ${escapeHtml(system.blocked || 0)}
实体 ${escapeHtml(entitySummary.cataloged_entity_total || 0)}
backlog ${escapeHtml(entitySummary.candidate_entity_total || 0)}
+ latest ${escapeHtml(system.latest_version || "-")}
+ version ${escapeHtml(system.version_sync_status || "-")}
${compact ? "" : `
@@ -629,6 +647,10 @@ function renderSystemCards(items, compact = false) {
队列与缺口
history complete ${escapeHtml(entitySummary.history_full_complete_count || 0)} · latest green ${escapeHtml(entitySummary.latest_green_count || 0)} · version gap ${escapeHtml(entitySummary.version_gap_entity_count || 0)}
+
+ 版本同步
+ latest synced ${escapeHtml(versionSummary.latest_version_synced_count || 0)} · source-gap ${escapeHtml(versionSummary.source_gap_count || 0)} · security versions ${escapeHtml(system.security_version_count || 0)}
+
${(topEntities.length || backlogPreview.length) ? `
@@ -816,6 +838,7 @@ function renderPanel(panelKey, title, meta, iconName, content) {
function renderCompletenessPanel(panelKey, compact = false) {
const completeness = state.completeness || state.summary?.completeness || {};
const entityCoverage = state.entityCompleteness || state.summary?.entity_coverage || completeness.entity_coverage || {};
+ const versionCoverage = state.versionCompleteness || state.summary?.version_coverage || completeness.version_coverage || {};
const sourceHealth = state.sourceHealth || completeness.source_health || {};
const systems = (state.completeness?.systems || []).map((system) => `
@@ -874,14 +897,32 @@ function renderCompletenessPanel(panelKey, compact = false) {
version mapped
${escapeHtml(entityCoverage.version_mapped_count || 0)}
+
+ latest version synced
+ ${escapeHtml(versionCoverage.latest_version_synced_count || 0)}
+
+
+ version source-gap
+ ${escapeHtml(versionCoverage.source_gap_count || 0)}
+
+
+ security versions
+ ${escapeHtml(versionCoverage.security_version_total || 0)}
+
+
+ lab enqueued
+ ${escapeHtml(versionCoverage.lab_enqueued_count || 0)}
+
${systems || `
暂无系统完整度数据。
`}
${compact ? "" : `
${failures.length ? `Ingest 未清零${escapeHtml(failures.join(" | "))}
` : ""}
@@ -1634,7 +1675,7 @@ async function loadData(preserveSelection = true) {
renderSyncState("loading", "刷新中", `本地时间 ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}`);
try {
- const [summary, runs, systems, entities, advisories, profiles, architecture, completeness, entityCompleteness, sourceHealth, alerts, monitorSummary] = await Promise.all([
+ const [summary, runs, systems, entities, advisories, profiles, architecture, completeness, entityCompleteness, versionCompleteness, sourceHealth, alerts, monitorSummary] = await Promise.all([
fetchJson("/summary.json"),
fetchJson("/runs.json"),
fetchJson("/systems.json"),
@@ -1644,6 +1685,7 @@ async function loadData(preserveSelection = true) {
fetchJson("/architecture.json"),
fetchJson("/data/completeness.json"),
fetchJson("/data/entity-completeness.json"),
+ fetchJson("/data/version-completeness.json"),
fetchJson("/data/source-health.json"),
fetchJson("/data/alerts.json"),
fetchJson("/data/monitor-summary.json")
@@ -1658,6 +1700,7 @@ async function loadData(preserveSelection = true) {
state.architecture = architecture;
state.completeness = completeness;
state.entityCompleteness = entityCompleteness;
+ state.versionCompleteness = versionCompleteness;
state.sourceHealth = sourceHealth;
state.alerts = alerts;
state.monitorSummary = monitorSummary;
diff --git a/scripts/lab/render.py b/scripts/lab/render.py
index 894bd0a7..e8e90089 100644
--- a/scripts/lab/render.py
+++ b/scripts/lab/render.py
@@ -1101,6 +1101,12 @@ def _write_dashboard_docs(architecture: Dict[str, Any]) -> None:
_safe_read_text(ROOT / "08-threat-intel" / "generated" / "entity-discovery-backlog.md", "entity discovery backlog has not been generated yet."),
"工作台内置镜像页:待编目 repo / 插件 / 包 backlog 与等待原因。",
),
+ (
+ "version-sync-report.html",
+ "安全相关版本同步报告",
+ _safe_read_text(ROOT / "08-threat-intel" / "generated" / "version-sync-report.md", "version sync report has not been generated yet."),
+ "工作台内置镜像页:安全相关版本历史、source-gap 与版本驱动 lab enqueue 摘要。",
+ ),
(
"coverage-matrix.html",
"覆盖矩阵镜像",
@@ -1440,12 +1446,22 @@ def render_dashboard(
for system_id, system in systems.items():
entity_summary = entity_summary_map.get(system_id, {})
+ version_summary = version_summary_map.get(system_id, {})
+ root_entity = next((item for item in entities_by_system.get(system_id, []) if item.get("entity_id") == system_id), {})
system["entity_summary"] = entity_summary
+ system["version_summary"] = version_summary
system["top_entities"] = entity_summary.get("top_entities", [])
system["backlog_preview"] = entity_summary.get("backlog_preview", [])
system["entity_total"] = entity_summary.get("cataloged_entity_total", 0)
system["entity_backlog"] = entity_summary.get("candidate_entity_total", 0)
system["entity_type_counts"] = entity_summary.get("entity_type_counts", {})
+ system["latest_version"] = root_entity.get("latest_version") or next(
+ (item.get("latest_version") for item in (version_summary.get("latest_versions") or []) if item.get("latest_version")),
+ "",
+ )
+ system["last_version_synced_at"] = root_entity.get("last_version_synced_at") or ""
+ system["version_sync_status"] = root_entity.get("version_sync_status") or ("source-gap" if version_summary.get("source_gap_count") else "green")
+ system["security_version_count"] = version_summary.get("security_version_count", 0)
recent_runs = sorted(runs, key=lambda item: item.get("finished_at") or "", reverse=True)[:100]
decorated_runs: List[Dict[str, Any]] = []
@@ -1511,6 +1527,7 @@ def render_dashboard(
"last_fully_green_run": source_health.get("last_fully_green_run"),
},
"entity_coverage": entity_completeness,
+ "version_coverage": version_completeness,
}
for item in merged_advisories:
status = item.get("verification_status", "triage-manual")
@@ -1555,6 +1572,10 @@ def render_dashboard(
"candidate_entity_total": entity_completeness.get("candidate_entity_total", 0),
"workflow_complete_count": entity_completeness.get("workflow_complete_count", 0),
"version_mapped_count": entity_completeness.get("version_mapped_count", 0),
+ "latest_version_synced_count": version_completeness.get("latest_version_synced_count", 0),
+ "version_source_gap_count": version_completeness.get("source_gap_count", 0),
+ "security_version_total": version_completeness.get("security_version_total", 0),
+ "lab_enqueued_count": version_completeness.get("lab_enqueued_count", 0),
}
write_json(DASHBOARD_DIR / "summary.json", summary)
@@ -1571,6 +1592,10 @@ def render_dashboard(
write_json(DASHBOARD_DIR / "data" / "entity-completeness.json", entity_completeness)
write_json(DASHBOARD_DIR / "data" / "entity-discovery-backlog.json", entity_backlog)
write_json(DASHBOARD_DIR / "data" / "entity-queues.json", entity_queues)
+ write_json(DASHBOARD_DIR / "data" / "version-completeness.json", version_completeness)
+ write_json(DASHBOARD_DIR / "data" / "version-backlog.json", version_backlog)
+ write_json(DASHBOARD_DIR / "data" / "release-index.json", release_index)
+ write_json(DASHBOARD_DIR / "data" / "lab-enqueue-summary.json", lab_enqueue_summary)
_write_testing_completeness_report(completeness)
architecture = _build_architecture_data(summary, source_map, repro_map)
write_json(DASHBOARD_DIR / "architecture.json", architecture)