Files
state-hub/dashboard/src/repos.md
tegwick 9f744dd7f3 feat(ep-td+dashboard): complete CUST-WP-0004 EP/TD tracking workstream
EP catalogue (all domains):
- EP-RAIL-001 ep_id patched (schema fix: add ep_id to EPUpdate)
- EP-RAIL-003 (git bare-repo mirrors) and EP-RAIL-004 (offsite secondary
  backup) registered from railiance-cluster/docs/backup-restore.md
- EP-CUST-003..007 ep_ids assigned to existing custodian EPs
- EP-CUST-008 (State Hub API auth) and EP-CUST-009 (update_workstream MCP
  tool) registered as new custodian extension points

TD catalogue (railiance — first 5 items):
- TD-RAIL-001: backup cron runs as root without audit trail (high/security)
- TD-RAIL-002: k3s kubeconfig world-readable mode 644 (medium/security)
- TD-RAIL-003: no Ansible role unit tests (medium/test)
- TD-RAIL-004: age key extracted via awk — fragile (medium/impl)
- TD-RAIL-005: etcd snapshot retention uncoordinated (low/impl)

Dashboard (T08 + T10):
- Extract API URL and POLL to src/components/config.js; all 15 pages
  now import from the shared module (contributions/goals keep custom POLL)
- Shared .kpi-infobox, .filter-bar, .filter-search/.filter-owner CSS
  moved to observablehq.config.js head <style> block; removed from 9 pages
- Build: 0 errors, 0 warnings

API (T09):
- progress.py: limit param now Query(100, le=1000) — prevents unbounded
  list requests; closes TD-CUST-004 for the only endpoint that had limit

CUST-WP-0004 marked completed (all 10 tasks done).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 01:40:52 +01:00

13 KiB

title
title
Repos
import {API} from "./components/config.js";
let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _workstreams = [];
try {
  [_repos, _domains, _sbom, _eps, _tds, _workstreams] = await Promise.all([
    fetch(`${API}/repos/`).then(r => r.ok ? r.json() : []),
    fetch(`${API}/domains/`).then(r => r.ok ? r.json() : []),
    fetch(`${API}/sbom/`).then(r => r.ok ? r.json() : []),
    fetch(`${API}/extension-points/`).then(r => r.ok ? r.json() : []),
    fetch(`${API}/technical-debt/`).then(r => r.ok ? r.json() : []),
    fetch(`${API}/workstreams/`).then(r => r.ok ? r.json() : []),
  ]);
} catch {}
const repos       = _repos       ?? [];
const domains     = _domains     ?? [];
const sbom        = _sbom        ?? [];
const eps         = _eps         ?? [];
const tds         = _tds         ?? [];
const workstreams = _workstreams ?? [];

// Lookups
const domainById   = Object.fromEntries(domains.map(d => [d.id, d]));
const domainBySlug = Object.fromEntries(domains.map(d => [d.slug, d]));

// Active "repo-integration-{slug}" workstreams — signals onboarding in progress
const integratingBySlug = Object.fromEntries(
  workstreams
    .filter(w => w.status === "active" && w.slug?.startsWith("repo-integration-"))
    .map(w => [w.slug.replace("repo-integration-", ""), w])
);

// Per-repo SBOM stats (from sbom entries)
const sbomByRepo = {};
for (const e of sbom) {
  if (!sbomByRepo[e.repo_id]) sbomByRepo[e.repo_id] = { count: 0, snapshot_at: e.snapshot_at };
  sbomByRepo[e.repo_id].count++;
}

// Per-domain counts
const epByDomain    = {};
const tdByDomain    = {};
const contribByDomain = {};

// EPs are domain-scoped
for (const ep of eps) {
  if (!ep.status || ep.status === "open" || ep.status === "in_progress") {
    epByDomain[ep.domain] = (epByDomain[ep.domain] ?? 0) + 1;
  }
}
for (const td of tds) {
  if (!td.status || td.status === "open" || td.status === "in_progress") {
    tdByDomain[td.domain] = (tdByDomain[td.domain] ?? 0) + 1;
  }
}
// Contributions: try to map via workstream → topic → domain (not available here; skip for now)
// Use domain slug from contributions' related_workstream if available — fallback: count by type only

// Build enriched repo rows
const repoRows = repos
  .filter(r => r.status === "active")
  .map(r => {
    const domain   = domainById[r.domain_id];
    const domSlug  = domain?.slug ?? "—";
    const domName  = domain?.name ?? "—";
    const sbomData = sbomByRepo[r.id];
    const hasSbom  = !!sbomData || !!r.last_sbom_at;
    const pkgCount = sbomData?.count ?? 0;
    const lastScan = r.last_sbom_at
      ? new Date(r.last_sbom_at).toLocaleDateString()
      : (sbomData?.snapshot_at ? new Date(sbomData.snapshot_at).toLocaleDateString() : null);
    const integrating = !!integratingBySlug[r.slug];
    return {
      _id:          r.id,
      _domSlug:     domSlug,
      _hasSbom:     hasSbom,
      _integrating: integrating,
      repo:         r.slug,
      domain:       domName,
      status:       integrating ? "⚙ integrating" : "ready",
      path:         r.local_path ?? "—",
      sbom:         hasSbom ? `✓ ${lastScan}` : "⚠ not ingested",
      pkgs:         pkgCount || (hasSbom ? "—" : 0),
      eps:          epByDomain[domSlug] ?? 0,
      tds:          tdByDomain[domSlug] ?? 0,
    };
  })
  .sort((a, b) => a._domSlug.localeCompare(b._domSlug) || a.repo.localeCompare(b.repo));

const gapCount        = repoRows.filter(r => !r._hasSbom).length;
const coveredCount    = repoRows.filter(r => r._hasSbom).length;
const integratingCount = repoRows.filter(r => r._integrating).length;

Repos

import {withDocHelp} from "./components/doc-overlay.js";
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/repos"); }
// Summary KPIs
display(html`<div class="kpi-row">
  <div class="card">
    <h3>Registered Repos</h3>
    <p class="big-num">${repoRows.length}</p>
  </div>
  <div class="card">
    <h3>Domains</h3>
    <p class="big-num">${new Set(repoRows.map(r => r._domSlug)).size}</p>
  </div>
  <div class="card ${integratingCount > 0 ? 'card-integrating' : ''}">
    <h3>Integrating</h3>
    <p class="big-num">${integratingCount}</p>
    <small>${integratingCount === 0 ? "✓ All repos integrated" : `⚙ ${integratingCount} onboarding`}</small>
  </div>
  <div class="card ${coveredCount < repoRows.length ? '' : ''}">
    <h3>SBOM Ingested</h3>
    <p class="big-num">${coveredCount} / ${repoRows.length}</p>
  </div>
  <div class="card ${gapCount > 0 ? 'card-warn' : 'card-ok'}">
    <h3>SBOM Gaps</h3>
    <p class="big-num">${gapCount}</p>
    <small>${gapCount === 0 ? "✓ All repos covered" : `⚠ ${gapCount} repo(s) not ingested`}</small>
  </div>
</div>`);

Coverage Map

// Returns a new "⚠ not ingested" span with a ? help button each time it's called.
function _sbomGap() {
  const el = html`<span class="sbom-warn sbom-gap-hint">⚠ not ingested</span>`;
  withDocHelp(el, "/docs/sbom");
  return el;
}

// Group by domain
const byDomain = {};
for (const r of repoRows) {
  (byDomain[r._domSlug] = byDomain[r._domSlug] ?? []).push(r);
}

const domainBlocks = Object.entries(byDomain).sort(([a], [b]) => a.localeCompare(b));

if (domainBlocks.length === 0) {
  display(html`<p style="color:gray">No repos registered. Run <code>make add-repo DOMAIN=&lt;slug&gt; SLUG=&lt;slug&gt; NAME="..." PATH=/path</code>.</p>`);
} else {
  display(html`<div class="domain-list">
    ${domainBlocks.map(([slug, rows]) => {
      const dom = domainBySlug[slug];
      const allCovered = rows.every(r => r._hasSbom);
      const hasEps  = (epByDomain[slug] ?? 0) > 0;
      const hasTds  = (tdByDomain[slug] ?? 0) > 0;
      return html`
        <div class="domain-block ${allCovered ? '' : 'domain-gap'}">
          <div class="domain-header">
            <span class="domain-name">${dom?.name ?? slug}</span>
            <span class="domain-chips">
              ${allCovered
                ? html`<span class="chip chip-ok">SBOM ✓</span>`
                : html`<span class="chip chip-warn">SBOM ⚠</span>`}
              ${hasEps
                ? html`<span class="chip chip-ok">EPs: ${epByDomain[slug]}</span>`
                : html`<span class="chip chip-neutral">EPs: —</span>`}
              ${hasTds
                ? html`<span class="chip chip-ok">TDs: ${tdByDomain[slug]}</span>`
                : html`<span class="chip chip-neutral">TDs: —</span>`}
            </span>
          </div>
          <table class="repo-table">
            <thead><tr>
              <th>Repo</th>
              <th>Status</th>
              <th>SBOM</th>
              <th>Packages</th>
              <th>Local path</th>
            </tr></thead>
            <tbody>
              ${rows.map(r => html`<tr class="${r._integrating ? 'row-integrating' : r._hasSbom ? '' : 'row-gap'}">
                <td class="repo-cell"><code>${r.repo}</code></td>
                <td>${r._integrating
                  ? html`<span class="chip chip-integrating">⚙ integrating</span>`
                  : html`<span class="chip chip-ok">ready</span>`}</td>
                <td class="${r._hasSbom ? 'sbom-ok' : 'sbom-warn'}">${r._hasSbom ? r.sbom : _sbomGap()}</td>
                <td>${r.pkgs}</td>
                <td class="path-cell">${r.path}</td>
              </tr>`)}
            </tbody>
          </table>
        </div>
      `;
    })}
  </div>`);
}

All Repos Table

const domainFilter = Inputs.select(["all", ...new Set(repoRows.map(r => r._domSlug)).values()], {label: "Domain", value: "all"});
const gapFilter    = Inputs.toggle({label: "Gaps only (no SBOM)", value: false});
display(html`<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem">${domainFilter}${gapFilter}</div>`);
const filteredRows = repoRows.filter(r =>
  (domainFilter.value === "all" || r._domSlug === domainFilter.value) &&
  (!gapFilter.value || !r._hasSbom)
);

display(Inputs.table(filteredRows.map(r => ({
  Repo:    r.repo,
  Domain:  r.domain,
  Status:  r.status,
  SBOM:    r.sbom,
  Pkgs:    r.pkgs,
  "EPs (domain)": r.eps || "—",
  "TDs (domain)": r.tds || "—",
  Path:    r.path,
})), {maxWidth: 1100}));

Onboard a New Repo

const _h2onboard = [...document.querySelectorAll("#observablehq-main h2")]
  .find(h => h.textContent.includes("Onboard a New Repo"));
if (_h2onboard) { _h2onboard.style.position = "relative"; withDocHelp(_h2onboard, "/docs/repo-integration"); }
display(html`<div class="onboard-panel">
  <div class="onboard-step">
    <span class="onboard-num">1</span>
    <div>
      <strong>Clone the repo locally</strong>
      <pre>git clone &lt;remote-url&gt; /path/to/repo</pre>
    </div>
  </div>
  <div class="onboard-step">
    <span class="onboard-num">2</span>
    <div>
      <strong>Register from the repo root</strong>
      <pre>cd /path/to/repo
custodian register-project --domain &lt;slug&gt;</pre>
      <p class="onboard-note">The custodian writes <code>CLAUDE.custodian.md</code>, registers the repo, and creates 4 onboarding tasks in the domain's topic.</p>
    </div>
  </div>
  <div class="onboard-step">
    <span class="onboard-num">3</span>
    <div>
      <strong>Open the repo in Claude Code and run /init</strong>
      <pre>cd /path/to/repo && claude</pre>
      <p class="onboard-note">Once Claude starts, run <code>/init</code> to trigger the integration. The repo agent reads <code>CLAUDE.custodian.md</code>, picks up the onboarding tasks, and integrates autonomously.</p>
    </div>
  </div>
  <div class="onboard-step">
    <span class="onboard-num">4</span>
    <div>
      <strong>Monitor here</strong>
      <p class="onboard-note">The <strong>⚙ integrating</strong> badge clears when the repo agent completes all 4 onboarding tasks and closes the workstream.</p>
    </div>
  </div>
</div>`);
<style> .kpi-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; } .card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; } .card-warn { border: 2px solid #e53935; } .card-ok { border: 2px solid #2e7d32; } .big-num { font-size: 2.2rem; font-weight: bold; margin: 0.25rem 0; } .domain-list { display: flex; flex-direction: column; gap: 1rem; margin-bottom: 1.5rem; } .domain-block { background: var(--theme-background-alt); border-radius: 8px; padding: 0.9rem 1rem; } .domain-gap { border-left: 4px solid #f0a500; } .domain-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.65rem; flex-wrap: wrap; } .domain-name { font-weight: 700; font-size: 1rem; } .domain-chips { display: flex; gap: 0.4rem; flex-wrap: wrap; } .chip { font-size: 0.72rem; font-weight: 600; border-radius: 10px; padding: 0.1rem 0.5rem; } .chip-ok { background: #e8f5e9; color: #2e7d32; } .chip-warn { background: #fff3cd; color: #856404; } .chip-neutral { background: var(--theme-background); color: gray; border: 1px solid var(--theme-foreground-faint); } .repo-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } .repo-table th { text-align: left; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: gray; padding: 0.3rem 0.5rem; border-bottom: 1px solid var(--theme-foreground-faint); } .repo-table td { padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--theme-foreground-faint); } .repo-table tr:last-child td { border-bottom: none; } .row-gap { background: #fffbf0; } .repo-cell { font-family: monospace; } .sbom-ok { color: #2e7d32; font-weight: 600; } .sbom-warn { color: #856404; font-weight: 600; } .sbom-gap-hint { position: relative; display: inline-block; padding-right: 1.8rem; } .path-cell { font-family: monospace; font-size: 0.78rem; color: gray; } .card-integrating { border: 2px solid #7c3aed; } .chip-integrating { background: #ede9fe; color: #5b21b6; } .row-integrating { background: #faf5ff; } .onboard-panel { display: flex; flex-direction: column; gap: 0; margin-top: 0.5rem; background: var(--theme-background-alt); border-radius: 8px; overflow: hidden; } .onboard-step { display: flex; gap: 1rem; align-items: flex-start; padding: 0.9rem 1.1rem; border-bottom: 1px solid var(--theme-foreground-faint, #eee); } .onboard-step:last-child { border-bottom: none; } .onboard-num { flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--theme-foreground-focus, #1a1a1a); color: var(--theme-background, white); font-size: 0.8rem; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 0.1rem; } .onboard-step strong { font-size: 0.9rem; display: block; margin-bottom: 0.3rem; } .onboard-step pre { background: var(--theme-background); border-radius: 4px; padding: 0.4rem 0.7rem; font-size: 0.8rem; overflow-x: auto; margin: 0 0 0.35rem; } .onboard-note { font-size: 0.82rem; color: var(--theme-foreground-muted, gray); margin: 0; line-height: 1.45; } </style>