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>
This commit is contained in:
2026-03-01 23:46:26 +01:00
parent 70c8e3cd51
commit 947c2e8824
22 changed files with 1468 additions and 25 deletions

View File

@@ -0,0 +1,158 @@
---
title: Dependencies
---
```js
const API = "http://127.0.0.1:8000";
const POLL = 15_000;
```
```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, rs] = await Promise.all([
fetch(`${API}/workstreams/`),
fetch(`${API}/topics/`),
fetch(`${API}/state/summary`),
]);
ok = rw.ok && rto.ok && rs.ok;
if (ok) {
const [wsList, topicList, summary] = await Promise.all([
rw.json(), rto.json(), rs.json(),
]);
const topicMap = Object.fromEntries(topicList.map(t => [t.id, t]));
wsMap = Object.fromEntries(wsList.map(w => [w.id, {
...w,
domain: 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));
}
})();
```
```js
const wsMap = depState.wsMap ?? {};
const edges = depState.edges ?? [];
const _ok = depState.ok ?? false;
const _ts = depState.ts;
```
# Dependencies
```js
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.
```js
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-infobox { background: var(--theme-background-alt, #f9f9f9); border: 1px solid var(--theme-foreground-faint, #e0e0e0); border-radius: 10px; padding: 0.75rem 1rem; position: relative; box-shadow: 0 1px 6px rgba(0,0,0,0.07); margin-bottom: 1.25rem; }
.kpi-infobox-title { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--theme-foreground-muted, #888); margin-bottom: 0.55rem; }
.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>