the-custodian/state-hub/dashboard/src/domains.md
tegwick 2cd061c1d1 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

7.7 KiB

title
Domains
import {API, POLL} from "./components/config.js";
const domainsState = (async function*() {
  while (true) {
    let domains = [], repos = [], ok = false;
    try {
      const [rd, rr] = await Promise.all([
        fetch(`${API}/domains/?status=all`),
        fetch(`${API}/repos/`),
      ]);
      ok = rd.ok && rr.ok;
      if (ok) {
        [domains, repos] = await Promise.all([rd.json(), rr.json()]);
      }
    } catch {}
    yield {domains, repos, ok, ts: new Date()};
    await new Promise(res => setTimeout(res, POLL));
  }
})();
const domains = domainsState.domains ?? [];
const repos   = domainsState.repos   ?? [];
const _ok     = domainsState.ok      ?? false;
const _ts     = domainsState.ts;

Domains

import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp}  from "./components/doc-overlay.js";
import {openEntityModal} from "./components/entity-modal.js";

// ── Live indicator ─────────────────────────────────────────────────────────────
const _liveEl = html`<div class="live-indicator">
  <span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
  ${_ok
    ? `Live · updated ${_ts?.toLocaleTimeString()}`
    : html`<span style="color:red">Offline — run: <code>make api</code></span>`}
</div>`;
withDocHelp(_liveEl, "/docs/live-data");
injectTocTop("live-indicator", _liveEl);

const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/domains"); }

// ── KPI row ────────────────────────────────────────────────────────────────────
const activeDomains = domains.filter(d => d.status === "active");
const archivedDomains = domains.filter(d => d.status === "archived");
const newestDomain = [...domains].sort((a, b) => b.created_at?.localeCompare(a.created_at ?? "") ?? 0)[0];

display(html`<div class="kpi-row-top">
  <div class="kpi-card">
    <div class="kpi-card-value">${domains.length}</div>
    <div class="kpi-card-label">total domains</div>
  </div>
  <div class="kpi-card">
    <div class="kpi-card-value">${activeDomains.length}</div>
    <div class="kpi-card-label">active</div>
  </div>
  <div class="kpi-card">
    <div class="kpi-card-value">${repos.length}</div>
    <div class="kpi-card-label">total repos</div>
  </div>
  <div class="kpi-card">
    <div class="kpi-card-value">${newestDomain?.name ?? "—"}</div>
    <div class="kpi-card-label">newest domain</div>
  </div>
</div>`);

Domain Cards

// Build repo index by domain_id
const reposByDomain = {};
for (const repo of repos) {
  if (!reposByDomain[repo.domain_id]) reposByDomain[repo.domain_id] = [];
  reposByDomain[repo.domain_id].push(repo);
}

if (domains.length === 0) {
  display(html`<p class="dim">No domains found. API may be offline.</p>`);
} else {
  display(html`<div class="domain-grid">${domains.map(d => {
    const domainRepos = reposByDomain[d.id] ?? [];
    return html`<div class="domain-card domain-status-${d.status} entity-row"
                     title="Click to view details">
      <div class="domain-card-header">
        <span class="domain-slug">${d.slug}</span>
        <span class="domain-status-badge domain-status-badge-${d.status}">${d.status}</span>
      </div>
      <div class="domain-name">${d.name}</div>
      ${d.description ? html`<div class="domain-desc">${d.description.slice(0, 160)}${d.description.length > 160 ? " …" : ""}</div>` : ""}
      <div class="domain-repos">
        ${domainRepos.length === 0
          ? html`<span class="no-repos">no repos registered</span>`
          : domainRepos.map(r => html`<div class="repo-row">
              <span class="repo-name">${r.name}</span>
              ${r.local_path ? html`<code class="repo-path">${r.local_path}</code>` : ""}
              ${r.remote_url ? html`<a class="repo-url" href=${r.remote_url} target="_blank">${r.remote_url.replace(/^https?:\/\//, "")}</a>` : ""}
            </div>`)
        }
      </div>
    </div>`;
  })}</div>`);
}