Files
state-hub/dashboard/src/dependencies.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

6.7 KiB

title
title
Dependencies
import {API, POLL} from "./components/config.js";
// Fetch workstreams + topics + summary (summary carries dep edges on open_workstreams)
const depState = (async function*() {
  while (true) {
    let wsMap = {}, edges = [], ok = false;
    try {
      const [rw, rto, rr, rs] = await Promise.all([
        fetch(`${API}/workstreams/`),
        fetch(`${API}/topics/`),
        fetch(`${API}/repos/`),
        fetch(`${API}/state/summary`),
      ]);
      ok = rw.ok && rto.ok && rr.ok && rs.ok;
      if (ok) {
        const [wsList, topicList, repoList, summary] = await Promise.all([
          rw.json(), rto.json(), rr.json(), rs.json(),
        ]);
        const topicMap = Object.fromEntries(topicList.map(t => [t.id, t]));
        const repoMap  = Object.fromEntries(repoList.map(r => [r.id, r]));
        wsMap = Object.fromEntries(wsList.map(w => [w.id, {
          ...w,
          // Prefer repo→domain (GEMS primary); fall back to topic→domain
          domain: repoMap[w.repo_id]?.domain_slug ?? topicMap[w.topic_id]?.domain_slug ?? "unknown",
        }]));
        // Build directed edge list from open_workstreams depends_on arrays
        for (const ow of (summary.open_workstreams ?? [])) {
          for (const depId of (ow.depends_on ?? [])) {
            edges.push({from_id: ow.id, to_id: depId});
          }
        }
      }
    } catch {}
    yield {wsMap, edges, ok, ts: new Date()};
    await new Promise(res => setTimeout(res, POLL));
  }
})();
const wsMap = depState.wsMap ?? {};
const edges  = depState.edges  ?? [];
const _ok    = depState.ok     ?? false;
const _ts    = depState.ts;

Dependencies

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

// ── KPI sidebar card ──────────────────────────────────────────────────────────
const _wsWithDeps = new Set([...edges.map(e => e.from_id), ...edges.map(e => e.to_id)]);
const _kpiBox = html`<div class="kpi-infobox">
  <div class="kpi-infobox-title">Dependencies</div>
  <div class="kpi-row">
    <span class="kpi-row-label">edges</span>
    <div class="kpi-row-right"><div class="kpi-row-value">${edges.length}</div></div>
  </div>
  <div class="kpi-row">
    <span class="kpi-row-label">workstreams involved</span>
    <div class="kpi-row-right"><div class="kpi-row-value">${_wsWithDeps.size}</div></div>
  </div>
</div>`;

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>`;

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

injectTocTop("dep-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);

Directed edges between active workstreams. An edge A → B means A cannot fully proceed until B reaches a satisfactory state.

if (edges.length === 0) {
  display(html`<p class="dim">No dependency edges registered.</p>`);
} else {
  const rows = edges.map(e => {
    const from = wsMap[e.from_id];
    const to   = wsMap[e.to_id];
    return {
      from_domain:  from?.domain ?? "—",
      from_title:   from?.title  ?? e.from_id,
      from_status:  from?.status ?? "—",
      to_domain:    to?.domain   ?? "—",
      to_title:     to?.title    ?? e.to_id,
      to_status:    to?.status   ?? "—",
    };
  });

  display(html`<table class="dep-table">
    <thead>
      <tr>
        <th>Depends-on domain</th>
        <th>Depends-on workstream</th>
        <th></th>
        <th>Blocked-by domain</th>
        <th>Blocked-by workstream</th>
        <th>Status</th>
      </tr>
    </thead>
    <tbody>${rows.map(r => html`
      <tr>
        <td class="dep-domain">${r.from_domain}</td>
        <td class="dep-title">${r.from_title}</td>
        <td class="dep-arrow">→</td>
        <td class="dep-domain">${r.to_domain}</td>
        <td class="dep-title">${r.to_title}</td>
        <td><span class="dep-status dep-status-${r.to_status}">${r.to_status}</span></td>
      </tr>
    `)}</tbody>
  </table>`);
}
<style> /* ── Live indicator ───────────────────────────────────────────────────────── */ .live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; } /* ── KPI infobox ──────────────────────────────────────────────────────────── */ .kpi-row { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.3rem 0; } .kpi-row + .kpi-row { border-top: 1px solid var(--theme-foreground-faint, #eee); } .kpi-row-label { font-size: 0.8rem; color: var(--theme-foreground-muted, #666); white-space: nowrap; } .kpi-row-right { text-align: right; } .kpi-row-value { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; } /* ── Dependency table ─────────────────────────────────────────────────────── */ .dep-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin-top: 0.5rem; } .dep-table th { text-align: left; padding: 0.4rem 0.75rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--theme-foreground-muted, #888); border-bottom: 2px solid var(--theme-foreground-faint, #e0e0e0); } .dep-table td { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--theme-foreground-faint, #eee); vertical-align: middle; } .dep-table tbody tr:hover { background: var(--theme-background-alt, #f9f9f9); } .dep-domain { font-size: 0.75rem; color: var(--theme-foreground-muted, #888); white-space: nowrap; } .dep-title { font-weight: 500; max-width: 22rem; } .dep-arrow { text-align: center; color: var(--theme-foreground-faint, #bbb); font-size: 1rem; } .dep-status { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 500; } .dep-status-active { background: #dcfce7; color: #166534; } .dep-status-completed { background: #f1f5f9; color: #475569; } .dep-status-blocked { background: #fee2e2; color: #991b1b; } .dep-status-archived { background: #f1f5f9; color: #9ca3af; } .dim { color: gray; font-style: italic; } </style>