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 ? "" : `
${icon("docs")}打开中文报告 ${icon("docs")}打开实体报告 + ${icon("docs")}打开版本报告 ${icon("json")}打开 completeness.json ${icon("json")}打开 entity-completeness.json + ${icon("json")}打开 version-completeness.json ${icon("json")}打开 source-health.json
${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)