diff --git a/state-hub/dashboard/src/data/repos.json.py b/state-hub/dashboard/src/data/repos.json.py
index 963e475..613bc7a 100644
--- a/state-hub/dashboard/src/data/repos.json.py
+++ b/state-hub/dashboard/src/data/repos.json.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""Observable data loader: fetches /repos/ from the API."""
+"""Observable data loader: fetches /repos/ enriched with SBOM snapshot stats."""
import json
import os
import urllib.request
@@ -7,9 +7,36 @@ import urllib.error
API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000").rstrip("/")
-try:
- with urllib.request.urlopen(f"{API_BASE}/repos/", timeout=10) as resp:
- data = json.loads(resp.read())
- print(json.dumps(data))
-except urllib.error.URLError as e:
- print(json.dumps({"error": str(e), "repos": []}))
+
+def fetch(url):
+ try:
+ with urllib.request.urlopen(url, timeout=10) as resp:
+ return json.loads(resp.read())
+ except urllib.error.URLError as e:
+ print(f"Warning: could not fetch {url}: {e}", flush=True)
+ return []
+
+
+repos = fetch(f"{API_BASE}/repos/")
+snapshots = fetch(f"{API_BASE}/sbom/snapshots/")
+
+# Build map: repo_id → {count, latest_at, latest_entry_count}
+snap_stats: dict = {}
+for s in snapshots:
+ rid = s["repo_id"]
+ if rid not in snap_stats:
+ snap_stats[rid] = {"count": 0, "latest_at": None, "latest_entry_count": 0}
+ snap_stats[rid]["count"] += 1
+ if snap_stats[rid]["latest_at"] is None or s["snapshot_at"] > snap_stats[rid]["latest_at"]:
+ snap_stats[rid]["latest_at"] = s["snapshot_at"]
+ snap_stats[rid]["latest_entry_count"] = s["entry_count"]
+
+# Enrich repos — fall back to snapshot data if denormalized field is missing
+for r in repos:
+ stats = snap_stats.get(str(r["id"]), {})
+ if not r.get("last_sbom_at") and stats.get("latest_at"):
+ r["last_sbom_at"] = stats["latest_at"]
+ r["sbom_snapshot_count"] = stats.get("count", 0)
+ r["sbom_entry_count"] = stats.get("latest_entry_count", 0)
+
+print(json.dumps(repos))
diff --git a/state-hub/dashboard/src/repo-sync.md b/state-hub/dashboard/src/repo-sync.md
index 84c1b5a..08d6a72 100644
--- a/state-hub/dashboard/src/repo-sync.md
+++ b/state-hub/dashboard/src/repo-sync.md
@@ -43,18 +43,28 @@ function syncColor(ts) {
const activeRepos = repos.filter(r => r.status === "active");
const staleCount = activeRepos.filter(r => !r.last_state_synced_at || ageMs(r.last_state_synced_at) > 86400000).length;
const freshCount = activeRepos.filter(r => r.last_state_synced_at && ageMs(r.last_state_synced_at) < 3600000).length;
+const sbomCount = activeRepos.filter(r => r.last_sbom_at).length;
+const noSbomCount = activeRepos.length - sbomCount;
```
```js
display(html`
-
+
${freshCount}
-
synced < 1h
+
state synced < 1h
${staleCount}
-
stale / never
+
state stale / never
+
+
+
${sbomCount}
+
SBOM ingested
+
+
+
${noSbomCount}
+
no SBOM yet
${activeRepos.length}
@@ -72,6 +82,7 @@ const table = html`
Domain
| Last Synced |
Last SBOM |
+ Entries |
Status |
@@ -82,7 +93,8 @@ const table = html`${r.slug}
| ${r.domain_slug} |
${fmtAge(r.last_state_synced_at)} |
- ${fmtAge(r.last_sbom_at)} |
+ ${fmtAge(r.last_sbom_at)} |
+ ${r.sbom_entry_count > 0 ? r.sbom_entry_count.toLocaleString() : "—"} |
${r.status}
|