2026-03-16 01:41:16 +01:00
|
|
|
|
---
|
|
|
|
|
|
title: Repo Sync Health
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# Repo Sync Health
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
const repoData = await FileAttachment("data/repos.json").json();
|
|
|
|
|
|
const inventory = await FileAttachment("data/gitea-inventory.json").json();
|
|
|
|
|
|
|
|
|
|
|
|
const repos = Array.isArray(repoData) ? repoData : (repoData.repos ?? []);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
// Helpers
|
|
|
|
|
|
function ageMs(ts) {
|
|
|
|
|
|
if (!ts) return Infinity;
|
|
|
|
|
|
return Date.now() - new Date(ts).getTime();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fmtAge(ts) {
|
|
|
|
|
|
if (!ts) return "never";
|
|
|
|
|
|
const ms = ageMs(ts);
|
|
|
|
|
|
const m = Math.floor(ms / 60000);
|
|
|
|
|
|
if (m < 60) return `${m}m ago`;
|
|
|
|
|
|
const h = Math.floor(m / 60);
|
|
|
|
|
|
if (h < 24) return `${h}h ago`;
|
|
|
|
|
|
return `${Math.floor(h / 24)}d ago`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncColor(ts) {
|
|
|
|
|
|
if (!ts) return "var(--theme-red)";
|
|
|
|
|
|
const h = ageMs(ts) / 3600000;
|
|
|
|
|
|
if (h < 1) return "var(--theme-green)";
|
|
|
|
|
|
if (h < 24) return "var(--theme-orange)";
|
|
|
|
|
|
return "var(--theme-red)";
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Registered Repos — Sync Status
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
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;
|
2026-03-19 01:34:02 +01:00
|
|
|
|
const sbomCount = activeRepos.filter(r => r.last_sbom_at).length;
|
|
|
|
|
|
const noSbomCount = activeRepos.length - sbomCount;
|
2026-03-16 01:41:16 +01:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
display(html`
|
2026-03-19 01:34:02 +01:00
|
|
|
|
<div style="display:flex;gap:1.5rem;margin-bottom:1.5rem;flex-wrap:wrap">
|
2026-03-16 01:41:16 +01:00
|
|
|
|
<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>
|
2026-03-19 01:34:02 +01:00
|
|
|
|
<div style="font-size:0.8rem;color:#666">state synced < 1h</div>
|
2026-03-16 01:41:16 +01:00
|
|
|
|
</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>
|
2026-03-19 01:34:02 +01:00
|
|
|
|
<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>
|
2026-03-16 01:41:16 +01:00
|
|
|
|
</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>
|
|
|
|
|
|
<div style="font-size:0.8rem;color:#666">total active</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
const table = html`<table style="width:100%;border-collapse:collapse;font-size:0.9rem">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr style="border-bottom:2px solid #ddd">
|
|
|
|
|
|
<th style="text-align:left;padding:6px 8px">Repo</th>
|
|
|
|
|
|
<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>
|
2026-03-19 01:34:02 +01:00
|
|
|
|
<th style="text-align:right;padding:6px 8px">Entries</th>
|
2026-03-16 01:41:16 +01:00
|
|
|
|
<th style="text-align:left;padding:6px 8px">Status</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
${activeRepos
|
|
|
|
|
|
.sort((a, b) => ageMs(a.last_state_synced_at) - ageMs(b.last_state_synced_at))
|
|
|
|
|
|
.map(r => html`<tr style="border-bottom:1px solid #eee">
|
|
|
|
|
|
<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>
|
2026-03-19 01:34:02 +01:00
|
|
|
|
<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>
|
2026-03-16 01:41:16 +01:00
|
|
|
|
<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>
|
|
|
|
|
|
</tr>`)
|
|
|
|
|
|
}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>`;
|
|
|
|
|
|
display(table);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Gitea Inventory — Unregistered Repos
|
|
|
|
|
|
|
|
|
|
|
|
_Repos on Gitea (`coulomb` org) not yet tracked by the state-hub._
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
const unregistered = inventory.unregistered ?? [];
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
if (unregistered.length === 0) {
|
|
|
|
|
|
display(html`<p style="color:var(--theme-green);font-weight:500">🎉 All Gitea repos are registered!</p>`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display(html`
|
|
|
|
|
|
<table style="width:100%;border-collapse:collapse;font-size:0.9rem">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr style="border-bottom:2px solid #ddd">
|
|
|
|
|
|
<th style="text-align:left;padding:6px 8px">Repo</th>
|
|
|
|
|
|
<th style="text-align:left;padding:6px 8px">Language</th>
|
|
|
|
|
|
<th style="text-align:left;padding:6px 8px">Description</th>
|
|
|
|
|
|
<th style="text-align:left;padding:6px 8px">Onboard</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
${unregistered.map(r => html`<tr style="border-bottom:1px solid #eee">
|
|
|
|
|
|
<td style="padding:6px 8px;font-weight:500">
|
|
|
|
|
|
<a href="${r.gitea_url}" target="_blank" style="color:inherit">${r.gitea_name}</a>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td style="padding:6px 8px;color:#777">${r.language || "—"}</td>
|
|
|
|
|
|
<td style="padding:6px 8px;color:#555">${r.description || "—"}</td>
|
|
|
|
|
|
<td style="padding:6px 8px;font-size:0.75rem;color:#999">
|
|
|
|
|
|
make register-project DOMAIN=? PROJECT_PATH=/home/worsch/${r.gitea_name}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>`)}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
`);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Hub-Only Repos
|
|
|
|
|
|
|
|
|
|
|
|
_Registered in the state-hub but no matching Gitea repo found._
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
const hubOnly = inventory.hub_only ?? [];
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
if (hubOnly.length === 0) {
|
|
|
|
|
|
display(html`<p style="color:#666">None — all hub repos have a Gitea counterpart.</p>`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display(html`<ul>${hubOnly.map(r => html`<li><code>${r.slug}</code> — domain: ${r.domain}, status: ${r.status}</li>`)}</ul>`);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
_Sync legend: 🟢 < 1h 🟠 1–24h 🔴 > 24h or never_
|
|
|
|
|
|
|
|
|
|
|
|
_Gitea token required for full inventory — set <code>GITEA_TOKEN</code> in <code>state-hub/.env</code>._
|