the-custodian/state-hub/dashboard/src/repos.md
tegwick 235355eb58 feat(dashboard): nav restructure, full context-help coverage, 11 new ref docs
Navigation:
- New order: Overview · Todo · Domains · Repos · Workstreams (collapsible,
  open:false, with atomic sub-entries: Decisions, Tasks, Debt, Extends,
  Dependencies) · Contributions · SBOM · Progress · Reference (collapsible)
- Reference section gains path:/reference landing page; all 18 doc pages
  listed in nav (alphabetical) and in reference.md table

New pages:
- todo.md — Internal / Ecosystem / Third-party todo classification
- dependencies.md — dependency edge table derived from state/summary
- reference.md — Reference landing page with full doc index

New reference doc pages (11):
  contributions, debt, dependencies, domains, extensions, overview,
  repos, tasks, todo + reference (meta) already added previously

doc-overlay.js — lazy bubblehelp tooltip:
- _titleCache Map + _fetchDocTitle(docPath): on first hover of any ?
  button, fetches the target doc page, parses <h1>, sets btn.title
- Native browser tooltip appears exactly on the ? circle on subsequent hover

Context-help wired on all 14 dashboard pages:
- h1 withDocHelp added to: index, todo, domains, repos, tasks, techdept,
  extensions, dependencies (contributions/workstreams/decisions/sbom/
  progress/reference were already wired)
- domains.md + repos.md: added missing withDocHelp import and live-data link
- tasks/techdept/extensions: removed duplicate _h1 const that caused
  SyntaxError: Identifier '_h1' has already been declared

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 23:46:26 +01:00

9.8 KiB

title
Repos
const API = "http://127.0.0.1:8000";
let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _contribs = [];
try {
  [_repos, _domains, _sbom, _eps, _tds, _contribs] = 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}/contributions/`).then(r => r.ok ? r.json() : []),
  ]);
} catch {}
const repos   = _repos   ?? [];
const domains = _domains ?? [];
const sbom    = _sbom    ?? [];
const eps     = _eps     ?? [];
const tds     = _tds     ?? [];
const contribs = _contribs ?? [];

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

// 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);
    return {
      _id:       r.id,
      _domSlug:  domSlug,
      _hasSbom:  hasSbom,
      repo:      r.slug,
      domain:    domName,
      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;

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 ${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

// 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>SBOM</th>
              <th>Packages</th>
              <th>Local path</th>
            </tr></thead>
            <tbody>
              ${rows.map(r => html`<tr class="${r._hasSbom ? '' : 'row-gap'}">
                <td class="repo-cell"><code>${r.repo}</code></td>
                <td class="${r._hasSbom ? 'sbom-ok' : 'sbom-warn'}">${r.sbom}</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,
  SBOM:    r.sbom,
  Pkgs:    r.pkgs,
  "EPs (domain)": r.eps || "—",
  "TDs (domain)": r.tds || "—",
  Path:    r.path,
})), {maxWidth: 1000}));

How to Ingest a Repo

display(html`<div class="howto">
  <h4>Register a new repo</h4>
  <pre>cd ~/the-custodian/state-hub
make add-repo DOMAIN=&lt;slug&gt; SLUG=&lt;repo-slug&gt; NAME="Display Name" PATH=/absolute/path</pre>

  <h4>Ingest SBOM (single ecosystem, auto-detect lockfile at root)</h4>
  <pre>make ingest-sbom REPO=&lt;slug&gt; REPO_PATH=/absolute/path</pre>

  <h4>Ingest SBOM (multi-ecosystem repo — scans all lockfiles recursively)</h4>
  <pre>make ingest-sbom REPO=&lt;slug&gt; SCAN=1 REPO_PATH=/absolute/path</pre>

  <h4>Infra-only repos (Ansible/shell — no lockfile)</h4>
  <p>Register the repo for inventory purposes. SBOM gap is expected and intentional.
  Terraform providers are tracked via <code>.terraform.lock.hcl</code> (auto-detected by <code>--scan</code>).</p>
</div>`);