fix(dashboard): enrich repo-sync page with live SBOM snapshot stats

repos.json.py now fetches /sbom/snapshots/ alongside /repos/ and
annotates each repo with sbom_snapshot_count, sbom_entry_count, and a
last_sbom_at fallback derived from actual snapshot data. This prevents
"LastSBOM=never" when the denormalized field is out of sync.

repo-sync.md gains SBOM KPI tiles (ingested vs no-SBOM), color-coded
SBOM age column (same green/orange/red scale as state sync), and an
entry count column showing packages from the latest snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 01:34:02 +01:00
parent bd1b01fdc0
commit 7bf3cf583a
2 changed files with 50 additions and 11 deletions

View File

@@ -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`
<div style="display:flex;gap:1.5rem;margin-bottom:1.5rem">
<div style="display:flex;gap:1.5rem;margin-bottom:1.5rem;flex-wrap:wrap">
<div style="padding:1rem 1.5rem;border-radius:8px;background:#f5f5f5;min-width:100px;text-align:center">
<div style="font-size:2rem;font-weight:700;color:var(--theme-green)">${freshCount}</div>
<div style="font-size:0.8rem;color:#666">synced &lt; 1h</div>
<div style="font-size:0.8rem;color:#666">state synced &lt; 1h</div>
</div>
<div style="padding:1rem 1.5rem;border-radius:8px;background:#f5f5f5;min-width:100px;text-align:center">
<div style="font-size:2rem;font-weight:700;color:var(--theme-red)">${staleCount}</div>
<div style="font-size:0.8rem;color:#666">stale / never</div>
<div style="font-size:0.8rem;color:#666">state stale / never</div>
</div>
<div style="padding:1rem 1.5rem;border-radius:8px;background:#f5f5f5;min-width:100px;text-align:center">
<div style="font-size:2rem;font-weight:700;color:var(--theme-green)">${sbomCount}</div>
<div style="font-size:0.8rem;color:#666">SBOM ingested</div>
</div>
<div style="padding:1rem 1.5rem;border-radius:8px;background:#f5f5f5;min-width:100px;text-align:center">
<div style="font-size:2rem;font-weight:700;color:${noSbomCount > 0 ? 'var(--theme-orange)' : 'var(--theme-green)'}">${noSbomCount}</div>
<div style="font-size:0.8rem;color:#666">no SBOM yet</div>
</div>
<div style="padding:1rem 1.5rem;border-radius:8px;background:#f5f5f5;min-width:100px;text-align:center">
<div style="font-size:2rem;font-weight:700">${activeRepos.length}</div>
@@ -72,6 +82,7 @@ const table = html`<table style="width:100%;border-collapse:collapse;font-size:0
<th style="text-align:left;padding:6px 8px">Domain</th>
<th style="text-align:left;padding:6px 8px">Last Synced</th>
<th style="text-align:left;padding:6px 8px">Last SBOM</th>
<th style="text-align:right;padding:6px 8px">Entries</th>
<th style="text-align:left;padding:6px 8px">Status</th>
</tr>
</thead>
@@ -82,7 +93,8 @@ const table = html`<table style="width:100%;border-collapse:collapse;font-size:0
<td style="padding:6px 8px;font-weight:500">${r.slug}</td>
<td style="padding:6px 8px;color:#555">${r.domain_slug}</td>
<td style="padding:6px 8px;color:${syncColor(r.last_state_synced_at)};font-weight:500">${fmtAge(r.last_state_synced_at)}</td>
<td style="padding:6px 8px;color:#777">${fmtAge(r.last_sbom_at)}</td>
<td style="padding:6px 8px;color:${syncColor(r.last_sbom_at)};font-weight:500">${fmtAge(r.last_sbom_at)}</td>
<td style="padding:6px 8px;text-align:right;color:#555">${r.sbom_entry_count > 0 ? r.sbom_entry_count.toLocaleString() : "—"}</td>
<td style="padding:6px 8px">
<span style="padding:2px 8px;border-radius:12px;font-size:0.75rem;background:${r.status === 'active' ? '#e8f5e9' : '#f5f5f5'};color:${r.status === 'active' ? '#2e7d32' : '#666'}">${r.status}</span>
</td>