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

@@ -2,29 +2,54 @@ export default {
root: "src",
title: "Custodian State Hub",
pages: [
// ── Overview ──────────────────────────────────────────────────────────────
{ name: "Overview", path: "/" },
{ name: "Workstreams", path: "/workstreams" },
{ name: "Tasks", path: "/tasks" },
{ name: "Decisions", path: "/decisions" },
{ name: "Progress", path: "/progress" },
{ name: "Todo", path: "/todo" },
// ── Organizational Entity Views ───────────────────────────────────────────
{ name: "Domains", path: "/domains" },
{ name: "Repos", path: "/repos" },
{ name: "Extension Points", path: "/extensions" },
{ name: "Technical Debt", path: "/techdept" },
{ name: "Repos", path: "/repos" },
{
name: "Workstreams",
path: "/workstreams",
collapsible: true,
open: false,
pages: [
{ name: "Decisions", path: "/decisions" },
{ name: "Tasks", path: "/tasks" },
{ name: "Debt", path: "/techdept" },
{ name: "Extends", path: "/extensions" },
{ name: "Dependencies", path: "/dependencies" },
],
},
// ── Functional Report Views ────────────────────────────────────────────────
{ name: "Contributions", path: "/contributions" },
{ name: "SBOM", path: "/sbom" },
{ name: "SBOM", path: "/sbom" },
{ name: "Progress", path: "/progress" },
// ── Reference ─────────────────────────────────────────────────────────────
{
name: "Reference",
path: "/reference",
collapsible: true,
open: false,
pages: [
{ name: "Decision Health", path: "/docs/decisions-kpi" },
{ name: "Decisions", path: "/docs/decisions" },
{ name: "Contributions", path: "/docs/contributions" },
{ name: "Decision Health", path: "/docs/decisions-kpi" },
{ name: "Decisions", path: "/docs/decisions" },
{ name: "Dependencies", path: "/docs/dependencies" },
{ name: "Domains", path: "/docs/domains" },
{ name: "Extension Points", path: "/docs/extensions" },
{ name: "Inter-Repo Communication", path: "/docs/inter-repo-communication" },
{ name: "Live Data", path: "/docs/live-data" },
{ name: "Progress Log", path: "/docs/progress-log" },
{ name: "SBOM", path: "/docs/sbom" },
{ name: "Workstream Health", path: "/docs/workstream-health-index" },
{ name: "Workstreams", path: "/docs/workstreams" },
{ name: "Live Data", path: "/docs/live-data" },
{ name: "Overview", path: "/docs/overview" },
{ name: "Progress Log", path: "/docs/progress-log" },
{ name: "Reference & Context Help", path: "/docs/reference" },
{ name: "Repos", path: "/docs/repos" },
{ name: "SBOM", path: "/docs/sbom" },
{ name: "Tasks", path: "/docs/tasks" },
{ name: "Technical Debt", path: "/docs/debt" },
{ name: "Todo", path: "/docs/todo" },
{ name: "Workstream Health", path: "/docs/workstream-health-index" },
{ name: "Workstreams", path: "/docs/workstreams" },
],
},
],

View File

@@ -12,7 +12,21 @@
* The ? button is invisible until the user hovers over the element.
*/
const _STYLE_ID = "doc-overlay-styles";
const _STYLE_ID = "doc-overlay-styles";
const _titleCache = new Map();
async function _fetchDocTitle(docPath) {
if (_titleCache.has(docPath)) return _titleCache.get(docPath);
try {
const res = await fetch(docPath);
if (!res.ok) return null;
const parser = new DOMParser();
const doc = parser.parseFromString(await res.text(), "text/html");
const title = doc.querySelector("h1")?.textContent?.trim() ?? null;
if (title) _titleCache.set(docPath, title);
return title;
} catch { return null; }
}
function _ensureStyles() {
if (typeof document === "undefined" || document.getElementById(_STYLE_ID)) return;
@@ -195,6 +209,14 @@ export function withDocHelp(element, docPath) {
btn.setAttribute("aria-label", "Open documentation");
btn.addEventListener("click", e => { e.stopPropagation(); _openOverlay(docPath); });
// Lazy-load the h1 of the target doc page as a native tooltip (bubblehelp)
btn.addEventListener("mouseenter", async () => {
if (btn.dataset.titleFetched) return;
btn.dataset.titleFetched = "1";
const title = await _fetchDocTitle(docPath);
if (title) btn.title = title;
}, {once: true});
element.append(btn);
return element;
}

View File

@@ -33,12 +33,17 @@ const _ts = contribState.ts;
```js
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
const _liveEl = html`<div class="live-indicator">
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
${_ok ? `Live · ${_ts?.toLocaleTimeString()}` : html`<span style="color:red">API offline</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/contributions"); }
```
```js

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>

View File

@@ -0,0 +1,120 @@
---
title: Contributions — Reference
---
# Contributions — Reference
Contributions track **outbound upstream work** — things the Custodian has
identified that belong in a repo it does not own or control. Each contribution
is a structured artifact filed locally in the repo's `contrib/` directory and
registered in the state hub so it is never lost.
---
## Contribution types
| Type | Full name | Use when |
|------|-----------|----------|
| `br` | Bug Report | You found a defect in an upstream tool or library |
| `fr` | Feature Request | You need functionality that upstream does not yet provide |
| `ep` | Extension Point | You identified a future enhancement opportunity in upstream code |
| `upr` | Upstream PR | You have written (or are writing) a patch for an upstream repo |
---
## Lifecycle
```
draft → submitted → acknowledged → accepted → merged
↘ ↘
rejected withdrawn
```
| Status | Meaning |
|--------|---------|
| **draft** | Artifact written locally; not yet sent upstream |
| **submitted** | Filed as a GitHub issue, PR, or email — awaiting upstream response |
| **acknowledged** | Upstream has seen it and responded (e.g. triaged, commented) |
| **accepted** | Upstream agreed to take action |
| **merged** | PR accepted and merged; issue resolved |
| **rejected** | Upstream declined; record kept for future reference |
| **withdrawn** | We decided not to pursue it |
Transitions are enforced by the API — you cannot skip stages arbitrarily.
`submitted_at` is stamped automatically when status moves to `submitted`;
`resolved_at` is stamped when status moves to `merged`, `rejected`, or `withdrawn`.
---
## Relation to the Todo classification
Contributions map directly to the **Third-party** class in the inter-repo
communication taxonomy:
| Todo class | Mechanism |
|------------|-----------|
| Internal | Workplan file + task in this repo's workstream |
| Ecosystem | State hub task with `[repo:<slug>]` prefix |
| **Third-party** | **Contribution artifact in `contrib/` + state hub registration** |
Contributions in `draft`, `submitted`, or `acknowledged` status appear as
open Third-party todos on the [Todo](/todo) page.
---
## File layout
Each artifact lives in the current repo under `contrib/`:
```
contrib/
bug-reports/ br-YYYY-MM-DD--<org>--<repo>--<slug>.md
feature-requests/ fr-YYYY-MM-DD--<org>--<repo>--<slug>.md
extension-points/ EP-<DOMAIN>-NNN--<org>--<repo>--<slug>.md
upstream-prs/ upr-YYYY-MM-DD--<org>--<repo>--<slug>.md
```
Templates live in `~/the-custodian/canon/standards/contrib-templates/`.
Convention details: `~/the-custodian/canon/standards/contribution-convention_v0.1.md`.
---
## Adding a contribution
**1. Write the artifact file** using the appropriate template.
**2. Register it in the state hub** via MCP:
```
register_contribution(
type = "fr",
title = "Add sidebar TOC injection API",
target_org = "observablehq",
target_repo = "framework",
body_path = "contrib/feature-requests/fr-2026-02-26--observablehq--framework--toc.md",
related_workstream_id = "<uuid>"
)
```
**3. Close the loop** when you file it upstream:
```
update_contribution_status(contribution_id="<uuid>", status="submitted")
```
**4. Keep updating** as upstream responds — `acknowledged`, `accepted`, `merged`.
---
## Kanban board
The Contributions page groups artifacts by status column. Only columns with at
least one entry are shown. The **⚠ follow-up banner** appears when any
contribution has been in `submitted` or `acknowledged` for an extended period
without further movement — a prompt to check in with upstream.
---
*Contributions are append-only. Rejected or withdrawn artifacts are retained as
institutional memory — they explain why certain approaches were tried and
dropped.*

View File

@@ -0,0 +1,91 @@
---
title: Technical Debt — Reference
---
# Technical Debt — Reference
The Technical Debt page tracks known quality compromises across all six project
domains — intentional shortcuts, design weaknesses, missing tests, and similar
issues that reduce codebase health but have been consciously deferred.
---
## Debt types
| Type | Examples |
|------|---------|
| **design** | Architectural decisions that should be revisited |
| **implementation** | Hacky or fragile code that works but shouldn't stay |
| **test** | Missing or incomplete test coverage |
| **docs** | Missing or outdated documentation |
| **dependencies** | Pinned old versions, unused packages, missing lockfiles |
| **performance** | Known bottlenecks not yet worth addressing |
| **security** | Hardcoded values, missing input validation, weak auth |
| **other** | Anything that doesn't fit the above |
---
## Severity levels
| Severity | Meaning |
|----------|---------|
| **critical** | Blocks release or poses an active risk |
| **high** | Should be resolved before the next major milestone |
| **medium** | Normal triage priority |
| **low** | Nice-to-fix; acceptable to defer indefinitely |
---
## Statuses
| Status | Meaning |
|--------|---------|
| **open** | Known and unaddressed |
| **in_progress** | Being actively worked on |
| **deferred** | Acknowledged but intentionally postponed |
| **resolved** | Fixed |
| **wont_fix** | Accepted as permanent — documented for future reference |
Items are sorted by status (open → in_progress → deferred → resolved → wont_fix)
then by severity (critical → high → medium → low) within each group.
---
## Filters
| Filter | Effect |
|--------|--------|
| **Status** | Multi-select |
| **Severity** | Multi-select |
| **Domain** | Multi-select |
| **Type** | Multi-select |
---
## Registering debt
Via MCP:
```
register_technical_debt(
domain = "custodian",
title = "Hard-coded API URL in data loaders",
debt_type = "implementation",
severity = "high",
description = "All data loaders use http://127.0.0.1:8000 directly. Should read from an env var or config.",
location = "state-hub/dashboard/src/data/*.json.py",
workstream_id = "<uuid>" # optional
)
```
```
update_td_status(td_uuid="<uuid>", status="resolved")
```
---
## Human-readable IDs
Each debt item carries a human-readable ID in the form `TD-<DOMAIN>-NNN`
(e.g. `TD-CUST-001`). IDs are optional at creation and auto-assigned if omitted.
They appear in the table for easy reference in commit messages and comments.

View File

@@ -0,0 +1,90 @@
---
title: Dependencies — Reference
---
# Dependencies — Reference
The Dependencies page shows the directed dependency graph between active
workstreams — which workstreams are waiting on others to reach a satisfactory
state before they can fully proceed.
---
## What is a dependency edge?
A dependency edge **A → B** means workstream A cannot fully proceed until
workstream B is in a satisfactory state (typically `completed` or `archived`).
Edges are used to model real sequencing constraints: for example, a shared
library must reach a stable release before downstream domains can build on it.
The Custodian's dependency order is:
```
Railiance → Markitect → Coulomb.social → Personhood / Foerster → Custodian
```
---
## Edge table
Each row shows:
| Column | Meaning |
|--------|---------|
| **Depends-on domain** | Domain of the dependent workstream (the one waiting) |
| **Depends-on workstream** | Title of the workstream that has the dependency |
| **→** | Direction arrow |
| **Blocked-by domain** | Domain of the prerequisite workstream |
| **Blocked-by workstream** | Title of the workstream that must complete first |
| **Status** | Current status of the prerequisite (green = active, grey = completed) |
---
## KPI sidebar card
Shows the total number of edges and the number of distinct workstreams involved
in at least one dependency relationship.
---
## Registering a dependency
Via MCP:
```
create_dependency(
from_workstream_id = "<uuid of dependent>",
to_workstream_id = "<uuid of prerequisite>",
description = "Cannot build auth layer until shared-library API is stable"
)
```
Via REST:
```bash
curl -X POST http://127.0.0.1:8000/workstreams/<from_id>/dependencies/ \
-H "Content-Type: application/json" \
-d '{"to_workstream_id": "<to_id>", "description": "..."}'
```
To list dependencies for a workstream:
```
list_dependencies(workstream_id="<uuid>")
```
---
## Cycle detection
The Workstream Health Index (WHI) includes a **Cycle Penalty Index (CPI)**
metric that detects circular dependencies using depth-first search. If CPI = 1,
a cycle exists and the WHI is penalised by 50%. The WHI KPI card on the
[Workstreams](/workstreams) page will display a cycle alert.
---
## Data source
Dependency edges are derived from the `depends_on` arrays on `open_workstreams`
in `GET /state/summary`. Polls every **15 seconds**.

View File

@@ -0,0 +1,82 @@
---
title: Domains — Reference
---
# Domains — Reference
The Domains page shows all registered project domains and the repositories
associated with each one. Domains are the top-level organisational unit of the
Custodian ecosystem.
---
## What is a domain?
A domain corresponds to one of the six tracked project areas:
| Slug | Project |
|------|---------|
| `custodian` | The Custodian agent system itself |
| `railiance` | DevOps & infrastructure reliability |
| `markitect` | Knowledge artifact management |
| `coulomb_social` | Co-creation marketplace |
| `personhood` | Rights & obligations framework |
| `foerster_capabilities` | Agency capability taxonomy |
Each domain has a slug (URL-friendly identifier), a human-readable name, an
optional description, and a status.
---
## Domain statuses
| Status | Meaning |
|--------|---------|
| **active** | Live domain — topics, workstreams, and tasks are being tracked |
| **archived** | Soft-deleted; no active work. Fails to archive if active topics exist |
---
## KPI row
Four counters at the top of the page:
| Counter | Meaning |
|---------|---------|
| Total domains | All registered domains regardless of status |
| Active | Domains with status `active` |
| Total repos | Sum of all registered repositories across all domains |
| Newest domain | Name of the most recently created domain |
---
## Domain cards
One card per domain showing:
- **Slug** — monospace identifier
- **Status badge** — green `active` or grey `archived`
- **Name** — display name
- **Description** — first 160 characters
- **Repos** — list of registered repositories for this domain, each showing name, local path, and remote URL
---
## Managing domains
Via MCP:
```
create_domain(slug="my_project", name="My Project", description="…")
rename_domain(slug="old_slug", new_slug="new_slug", new_name="New Name")
archive_domain(slug="my_project") # fails if active topics exist
```
Via Makefile:
```bash
make add-domain SLUG=my_project NAME="My Project"
make rename-domain OLD_SLUG=my_project NEW_SLUG=myproject NEW_NAME="My Project"
```
*Domains are never hard-deleted — only archived.*

View File

@@ -0,0 +1,102 @@
---
title: Extension Points — Reference
---
# Extension Points — Reference
The Extension Points page tracks known future enhancement opportunities across
all six domains — design forks the system *could* take, parked deliberately
for later consideration rather than acted on immediately.
---
## What is an extension point?
An extension point (EP) captures a place in the design where additional
capability could be added — an API surface that could be extended, a schema
that could grow, an integration that could be built. Recording an EP
acknowledges the opportunity without committing to it.
Extension points are distinct from technical debt: debt is a known compromise
that should be fixed; EPs are optional future directions that may or may not
be pursued.
---
## EP types
| Type | Examples |
|------|---------|
| **api** | New endpoints, query parameters, response fields |
| **schema** | New tables, columns, relationships |
| **mcp** | New MCP tools or resources |
| **dashboard** | New pages, charts, or components |
| **architecture** | Structural changes to the system design |
| **integration** | Connections to external systems |
| **other** | Anything that doesn't fit the above |
---
## Statuses
| Status | Meaning |
|--------|---------|
| **open** | Identified, not yet acted on |
| **in_progress** | Being implemented as part of an active workstream |
| **addressed** | The capability has been built |
| **deferred** | Intentionally postponed |
| **wont_fix** | Decided not to pursue — kept for documentation |
Items are sorted by status (open → in_progress → deferred → addressed → wont_fix)
then by priority (critical → high → medium → low).
---
## Priorities
| Priority | Meaning |
|----------|---------|
| **critical** | Needed to unblock other work |
| **high** | High-value enhancement for the near term |
| **medium** | Would be useful but not urgent |
| **low** | Speculative or long-horizon idea |
---
## Filters
| Filter | Effect |
|--------|--------|
| **Status** | Multi-select |
| **Priority** | Multi-select |
| **Domain** | Multi-select |
| **Type** | Multi-select |
---
## Registering an extension point
Via MCP:
```
register_extension_point(
domain = "custodian",
title = "Configurable poll interval per dashboard page",
ep_type = "dashboard",
priority = "low",
description = "Each page hard-codes POLL = 15_000. An env var or per-page config would allow slowing down low-priority pages to reduce API load.",
location = "state-hub/dashboard/src/*.md",
workstream_id = "<uuid>" # optional
)
```
```
update_ep_status(ep_uuid="<uuid>", status="addressed")
```
---
## Human-readable IDs
Each EP carries an ID in the form `EP-<DOMAIN>-NNN` (e.g. `EP-CUST-001`).
IDs are optional at creation and auto-assigned if omitted.

View File

@@ -0,0 +1,84 @@
---
title: Overview — Reference
---
# Overview — Reference
The Overview page is the operational home screen of the Custodian State Hub.
It shows the live health of the entire ecosystem at a glance — active work,
blocking decisions, and system-derived next-step suggestions.
---
## Sections
### Open Workstreams by Domain
A horizontal stacked bar chart showing every active workstream across all six
domains. Each bar is broken into four task-status segments:
| Colour | Segment |
|--------|---------|
| green | done |
| blue | in progress |
| orange-red | blocked |
| light grey | todo |
The left axis shows domain labels (one per group of workstreams). The `done/total`
count is printed to the right of each bar. Workstreams with no tasks yet show
a grey "— no tasks yet" label.
### Contribution & SBOM Health
Three summary cards linked to the Contributions and SBOM pages:
| Card | Shows |
|------|-------|
| **Contributions** | Total artifact count; orange warning if any are awaiting upstream response |
| **Licence Risk** | Count of SBOM packages with copyleft licences in direct dependencies |
| **SBOM** | Breakdown by contribution type (BR / FR / EP / UPR) |
### Status
Four metric cards:
| Card | Meaning |
|------|---------|
| **Active Workstreams** | Count of non-completed, non-archived workstreams |
| **Blocking Decisions** | Pending decisions with status `open` or `escalated` — orange border if > 0 |
| **Blocked Tasks** | Click to expand the list with blocking reasons |
| **Events Today** | Progress events created on today's date |
### What's next?
System-derived action suggestions from `GET /state/next_steps`. Suggestions are
generated when a decision is resolved or a workstream dependency is cleared, and
they point to the first open task in the relevant workstream. These are derived
on request and never persisted.
### Blocking Decisions
Inline resolution form for each pending decision. Expand a card, enter a
rationale and "decided by" name, and click **Record & close**. The decision is
resolved via `POST /decisions/{id}/resolve` and disappears from the list
without a page reload.
### Registered Projects
Table of projects registered with `make register-project`, sourced from
`milestone` progress events whose summary starts with
`"Project registered with State Hub:"`.
### Recent Activity
Last 20 progress events across all domains, showing time, event type, author,
and summary.
---
## Data source
Polls `GET /state/summary` every **15 seconds**. Blocking decisions are fetched
separately via `GET /decisions/?decision_type=pending` and only re-fetched
after a successful resolve action — this prevents the inline form from being
wiped on every poll.

View File

@@ -0,0 +1,119 @@
---
title: Reference & Context Help — Reference
---
# Reference & Context Help
The **Reference** section is a collection of in-depth documentation pages
explaining the data model, design conventions, and mechanics of each dashboard
view. Reference pages are readable as standalone articles and also surfaced
inline via the **? context-help button** on dashboard pages.
---
## The ? context-help button
Every dashboard page exposes one or more **?** buttons — small circular
controls that open the relevant reference page in an overlay without leaving
the current view.
### Where ? buttons appear
| Location | Opens |
|----------|-------|
| Page **h1** heading | Reference page for that dashboard view |
| **KPI sidebar cards** | Reference page for the specific metric shown |
| **Live indicator** | [Live Data](/docs/live-data) — poll interval, offline recovery |
### How to use it
1. Hover over the element — the **?** button fades in at the top-right corner.
2. Click **?** — the reference page opens in a modal overlay.
3. Read the docs, then dismiss with **✕ close**, **Esc**, or by clicking the
backdrop.
The overlay does not interrupt the live data polling loop — the dashboard
continues refreshing in the background while the overlay is open.
---
## Overlay behaviour
| Detail | Value |
|--------|-------|
| Size | `min(780px, 92vw)` wide · `82vh` tall |
| Content | Observable Framework page rendered in an `<iframe>` |
| Dismiss | ✕ button · Esc key · click outside the box |
| Animation | Fade-in backdrop + slide-up box (150 ms) |
| Stacking | `z-index: 9000` — always above dashboard content |
Only one overlay can be open at a time. Opening a second **?** replaces the
first automatically.
---
## Adding ? help to a new page
The helper is exported from `src/components/doc-overlay.js`:
```js
import {withDocHelp} from "./components/doc-overlay.js";
```
**On a page h1:**
```js
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/my-page"); }
```
**On a sidebar card or other element** (must have `position: relative`):
```js
const _card = html`<div class="kpi-infobox" style="position:relative">…</div>`;
withDocHelp(_card, "/docs/my-page");
```
`withDocHelp(element, docPath)` mutates the element in place and returns it,
so it can be chained directly before `display()` or `injectTocTop()`.
---
## Writing a reference page
Reference pages live in `state-hub/dashboard/src/docs/` and follow a consistent
structure:
```
---
title: Topic — Reference
---
# Topic — Reference
One-sentence purpose statement.
---
## Section 1
## Section 2
---
*Footer note (optional — e.g. governance constraint, append-only policy).*
```
**Conventions:**
- Title frontmatter: `"Topic — Reference"` (em dash, not hyphen)
- Sections separated by `---` horizontal rules
- Tables preferred over prose lists for structured data
- Code blocks for MCP tool calls and REST examples
- No live data fetching — reference pages are static
After writing the page, register it in two places:
1. **`observablehq.config.js`** — add to the Reference `pages` array (alphabetical)
2. **`src/reference.md`** — add a row to the dashboard-pages table

View File

@@ -0,0 +1,83 @@
---
title: Repos — Reference
---
# Repos — Reference
The Repos page shows every repository registered in the Custodian ecosystem,
their SBOM ingestion status, and a domain-grouped coverage map.
---
## What is a managed repo?
A managed repo is a git repository that has been registered with the state hub
via `make add-repo` or `register_repo()`. Registration records the repo's slug,
domain, local path, and optional remote URL. Once registered, the repo can
receive SBOM ingestion and is eligible for the ADR-001 workplan validator.
---
## KPI row
| Card | Meaning |
|------|---------|
| **Registered Repos** | Active repos only (status = active) |
| **Domains** | Count of distinct domain slugs across registered repos |
| **SBOM Ingested** | Repos with at least one SBOM snapshot |
| **SBOM Gaps** | Repos with no ingested SBOM — red border when > 0 |
---
## Coverage Map
Groups repos by domain. Each domain block shows:
- **Domain name** with SBOM, EP, and TD chip indicators
- **SBOM chip** — green ✓ if all repos in the domain are ingested, amber ⚠ if any gap exists
- **EPs chip** — count of open/in-progress extension points for this domain
- **TDs chip** — count of open/in-progress technical debt items for this domain
- **Repo table** — one row per repo with SBOM status, package count, and local path
Rows with no SBOM are highlighted in amber.
---
## Filters
| Filter | Effect |
|--------|--------|
| **Domain** | Show repos for a single domain only |
| **Gaps only** | Toggle to show only repos without an ingested SBOM |
---
## Ingesting a repo's SBOM
```bash
# Register a new repo
cd ~/the-custodian/state-hub
make add-repo DOMAIN=<slug> SLUG=<repo-slug> NAME="Display Name" PATH=/absolute/path
# Ingest SBOM (auto-detects lockfile at repo root)
make ingest-sbom REPO=<slug> REPO_PATH=/absolute/path
# Multi-ecosystem repo — scan all lockfiles recursively
make ingest-sbom REPO=<slug> SCAN=1 REPO_PATH=/absolute/path
```
Supported lockfile formats: `uv.lock`, `requirements.txt`, `package-lock.json`,
`yarn.lock`, `Cargo.lock`, `.terraform.lock.hcl`.
---
## Infra-only repos
Repos with no lockfile (Ansible, shell scripts) can be registered for inventory
purposes. The SBOM gap is expected and can be left as-is. Terraform providers
are auto-detected via `.terraform.lock.hcl` when using `--scan`.
---
*SBOM snapshots are replaced on each ingest — not appended. The last ingestion
timestamp is recorded on the managed_repo row.*

View File

@@ -0,0 +1,95 @@
---
title: Tasks — Reference
---
# Tasks — Reference
The Tasks page shows all tasks across every workstream and domain, with live
filtering, a status distribution chart, and a blocked-tasks highlight section.
---
## Task statuses
| Status | Meaning |
|--------|---------|
| **todo** | Not yet started |
| **in_progress** | Actively being worked on |
| **blocked** | Cannot proceed — has a blocking reason |
| **done** | Completed |
| **cancelled** | Dropped; not counted toward totals |
---
## Task priorities
| Priority | Use for |
|----------|---------|
| **critical** | Must be resolved immediately; blocks a release or governance gate |
| **high** | Should be resolved this session or next |
| **medium** | Normal backlog priority |
| **low** | Nice-to-have; deferred is acceptable |
---
## Filter bar
| Filter | Effect |
|--------|--------|
| **Status** | Multi-select — show only chosen statuses |
| **Priority** | Multi-select — show only chosen priorities |
| **Domain** | Multi-select — filter by domain slug |
| **Assignee** | Case-insensitive substring match on the assignee field |
All filters are applied client-side after each poll.
---
## Status Distribution chart
A horizontal bar chart (Observable Plot) showing the count of filtered tasks
per status, colour-coded:
| Colour | Status |
|--------|--------|
| grey-blue | todo |
| blue | in_progress |
| red | blocked |
| green | done |
| light grey | cancelled |
---
## Blocked Tasks section
Shows cards for every task currently in `blocked` status within the active
filter. Each card displays:
- Priority badge and status
- Domain and workstream context
- Task title
- Blocking reason (amber background)
---
## KPI sidebar card
Shows four counts for the unfiltered dataset: open (todo + in_progress +
blocked), blocked, in progress, done, and a done-% of total.
---
## Sorting
Tasks are sorted by status (blocked first, then in_progress, todo, done,
cancelled) then by priority (critical → high → medium → low) within each
status group.
---
## Data sources
Polls every **15 seconds**:
- `GET /tasks/?limit=500`
- `GET /workstreams/`
- `GET /topics/`

View File

@@ -0,0 +1,71 @@
---
title: Todo — Reference
---
# Todo — Reference
The Todo page shows the Custodian's own open work items classified by where the
work belongs — inside this repo, routed from another ecosystem repo, or aimed
at an upstream third-party project.
This page is the session-start orientation surface for the `the-custodian` repo.
See [Inter-Repo Communication](/docs/inter-repo-communication) for the full
boundary rule and routing workflows.
---
## The three classes
### Internal
Open tasks (`todo`, `in_progress`, `blocked`) in **custodian domain workstreams**
whose title does not contain a `[repo:]` routing prefix.
These are tasks this agent is directly responsible for and can address within
the current repo.
### Ecosystem (inbound)
Tasks from **any workstream** whose title contains `[repo:the-custodian]`.
These tasks were created by agents in other repos to route work to the custodian.
When a task with this prefix appears, the session protocol picks it up, the
custodian creates a workplan file (ADR-001), and addresses the work here.
### Third-party (outbound)
Contribution artifacts in **open status** (`draft`, `submitted`, `acknowledged`).
These represent work the custodian has identified for upstream repos it does not
own. They remain on the todo list until the upstream loop is closed (`merged`,
`rejected`, or `withdrawn`).
---
## Classification decision table
| Situation | Class | Action |
|-----------|-------|--------|
| Bug found in this repo | Internal | Fix it directly |
| Work needed in another registered repo | Ecosystem | `create_task(..., title="[repo:<slug>] ...")` |
| Bug found in an upstream library | Third-party | Create BR artifact + `register_contribution` |
| Feature needed from upstream | Third-party | Create FR artifact + `register_contribution` |
---
## KPI sidebar card
Shows the count of open items in each class at a glance. The `?` button links
to this reference page.
---
## Data sources
Polls every **15 seconds**:
- `GET /tasks/?limit=500` — all tasks
- `GET /workstreams/` — for domain + title context
- `GET /topics/` — for domain slug resolution
- `GET /contributions/` — for third-party todos
*Classification is done client-side — no server-side filter endpoint required.*

View File

@@ -38,6 +38,7 @@ const _ts = domainsState.ts;
```js
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
import {openEntityModal} from "./components/entity-modal.js";
// ── Live indicator ─────────────────────────────────────────────────────────────
@@ -47,8 +48,12 @@ const _liveEl = html`<div class="live-indicator">
? `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");

View File

@@ -127,11 +127,11 @@ const _liveEl = html`<div class="live-indicator">
</div>`;
withDocHelp(_liveEl, "/docs/live-data");
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/extensions"); }
injectTocTop("ep-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) _h1.style.position = "relative";
```
## By Type & Status

View File

@@ -79,6 +79,9 @@ const _liveEl = html`<div class="live-indicator">
</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/overview"); }
```
```js

View File

@@ -0,0 +1,47 @@
---
title: Reference
---
```js
import {withDocHelp} from "./components/doc-overlay.js";
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/reference"); }
```
# Reference
In-depth documentation for each dashboard page, data model, and design
convention used in the Custodian State Hub.
---
## Dashboard pages
| Page | What it covers |
|------|---------------|
| [Contributions](/docs/contributions) | Contribution types, lifecycle, third-party todo workflow |
| [Decision Health](/docs/decisions-kpi) | KPI formula, avg resolve time, open-age colour thresholds |
| [Decisions](/docs/decisions) | Decision types, statuses, escalation rules, filter bar |
| [Dependencies](/docs/dependencies) | Dependency edges, registration, cycle detection |
| [Domains](/docs/domains) | Domain model, statuses, cards with repos |
| [Extension Points](/docs/extensions) | EP types, statuses, priorities, registration |
| [Inter-Repo Communication](/docs/inter-repo-communication) | Boundary rule, Internal/Ecosystem/Third-party taxonomy, routing workflows |
| [Live Data](/docs/live-data) | Poll interval, live indicator states, offline recovery |
| [Overview](/docs/overview) | State summary sections, workstream chart, blocking decisions, next steps |
| [Progress Log](/docs/progress-log) | Event types, append-only policy, session protocol |
| [Repos](/docs/repos) | Repo registry, SBOM coverage map, ingestion commands |
| [SBOM](/docs/sbom) | Lockfile ingestion, licence report, copyleft detection |
| [Tasks](/docs/tasks) | Task statuses, priorities, filter bar, status distribution chart |
| [Technical Debt](/docs/debt) | Debt types, severities, statuses, registration |
| [Todo](/docs/todo) | Internal/Ecosystem/Third-party classification, data sources |
| [Workstream Health](/docs/workstream-health-index) | WHI formula, six base metrics, per-domain breakdown |
| [Workstreams](/docs/workstreams) | Workstream statuses, dependency edges, WHI KPI card |
---
## Meta
| Topic | What it covers |
|-------|---------------|
| [Reference & Context Help](/docs/reference) | How reference pages work; the ? context-help button |

View File

@@ -92,6 +92,12 @@ const coveredCount = repoRows.filter(r => r._hasSbom).length;
# Repos
```js
import {withDocHelp} from "./components/doc-overlay.js";
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/repos"); }
```
```js
// Summary KPIs
display(html`<div class="kpi-row">

View File

@@ -136,12 +136,12 @@ const _liveEl = html`<div class="live-indicator">
</div>`;
withDocHelp(_liveEl, "/docs/live-data");
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/tasks"); }
// ── Inject into TOC sidebar ───────────────────────────────────────────────────
injectTocTop("task-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) _h1.style.position = "relative";
```
## Status Distribution

View File

@@ -137,11 +137,11 @@ const _liveEl = html`<div class="live-indicator">
</div>`;
withDocHelp(_liveEl, "/docs/live-data");
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/debt"); }
injectTocTop("td-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) _h1.style.position = "relative";
```
## By Type & Severity

235
dashboard/src/todo.md Normal file
View File

@@ -0,0 +1,235 @@
---
title: Todo
---
```js
const API = "http://127.0.0.1:8000";
const POLL = 15_000;
const THIS_REPO = "the-custodian";
```
```js
// Live poll: tasks + workstreams + topics + contributions
const todoState = (async function*() {
while (true) {
let tasks = [], contribs = [], wsMap = {}, ok = false;
try {
const [rt, rw, rto, rc] = await Promise.all([
fetch(`${API}/tasks/?limit=500`),
fetch(`${API}/workstreams/`),
fetch(`${API}/topics/`),
fetch(`${API}/contributions/`),
]);
ok = rt.ok && rw.ok && rto.ok && rc.ok;
if (ok) {
const [taskList, wsList, topicList, contribList] = await Promise.all([
rt.json(), rw.json(), rto.json(), rc.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",
}]));
tasks = taskList.map(t => ({
...t,
workstream_title: wsMap[t.workstream_id]?.title ?? "—",
domain: wsMap[t.workstream_id]?.domain ?? "unknown",
}));
contribs = contribList;
}
} catch {}
yield {tasks, contribs, ok, ts: new Date()};
await new Promise(res => setTimeout(res, POLL));
}
})();
```
```js
const tasks = todoState.tasks ?? [];
const contribs = todoState.contribs ?? [];
const _ok = todoState.ok ?? false;
const _ts = todoState.ts;
```
```js
// ── Classify tasks ────────────────────────────────────────────────────────────
const OPEN_STATUSES = new Set(["todo", "in_progress", "blocked"]);
// Internal: custodian domain, open, no [repo:] routing prefix
const internal = tasks.filter(t =>
OPEN_STATUSES.has(t.status) &&
t.domain === "custodian" &&
!t.title.includes("[repo:")
);
// Ecosystem inbound: tasks routed to this repo from any domain
const ecosystem = tasks.filter(t =>
OPEN_STATUSES.has(t.status) &&
t.title.toLowerCase().includes(`[repo:${THIS_REPO}]`)
);
// Third-party: open contributions (outbound work for upstream repos)
const thirdParty = contribs.filter(c =>
["draft", "submitted", "acknowledged"].includes(c.status)
);
```
# Todo
```js
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
// ── KPI sidebar card ──────────────────────────────────────────────────────────
const _kpiBox = html`<div class="kpi-infobox">
<div class="kpi-infobox-title">Todo Summary</div>
<div class="kpi-row">
<span class="kpi-row-label">internal</span>
<div class="kpi-row-right">
<div class="kpi-row-value">${internal.length}</div>
</div>
</div>
<div class="kpi-row">
<span class="kpi-row-label">ecosystem (inbound)</span>
<div class="kpi-row-right">
<div class="kpi-row-value">${ecosystem.length}</div>
</div>
</div>
<div class="kpi-row">
<span class="kpi-row-label">third-party (outbound)</span>
<div class="kpi-row-right">
<div class="kpi-row-value">${thirdParty.length}</div>
</div>
</div>
</div>`;
// ── 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("todo-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/todo"); }
```
---
## Internal
Work fully addressable within this repo. Open tasks in custodian workstreams
without a cross-repo routing prefix.
```js
const PRIORITY_ORDER = {critical: 0, high: 1, medium: 2, low: 3};
const STATUS_ORDER = {blocked: 0, in_progress: 1, todo: 2};
function sortTasks(arr) {
return [...arr].sort((a, b) => {
const sd = (STATUS_ORDER[a.status] ?? 9) - (STATUS_ORDER[b.status] ?? 9);
if (sd !== 0) return sd;
return (PRIORITY_ORDER[a.priority] ?? 9) - (PRIORITY_ORDER[b.priority] ?? 9);
});
}
function renderTaskList(arr) {
if (arr.length === 0) return html`<p class="dim">No open todos in this category. ✓</p>`;
return html`<div class="task-list">${sortTasks(arr).map(t => html`
<div class="task-item status-${t.status}">
<div class="task-item-header">
<span class="task-badge task-priority-${t.priority}">${t.priority}</span>
<span class="task-status-chip status-chip-${t.status}">${t.status.replace("_", " ")}</span>
<span class="task-context task-ws-name">${t.workstream_title}</span>
${t.assignee ? html`<span class="task-assignee">@${t.assignee}</span>` : ""}
</div>
<div class="task-title">${t.title}</div>
${t.blocking_reason ? html`<div class="task-blocking-reason">⊘ ${t.blocking_reason}</div>` : ""}
</div>
`)}</div>`;
}
display(renderTaskList(internal));
```
---
## Ecosystem
Inbound tasks routed to this repo by other agents via the
`[repo:${THIS_REPO}]` prefix convention.
```js
display(renderTaskList(ecosystem));
```
---
## Third-Party
Outbound work for upstream repos — open contribution artifacts
(bug reports, feature requests, extension points, upstream PRs).
```js
const TYPE_LABEL = {br: "Bug Report", fr: "Feature Request", ep: "Extension Point", upr: "Upstream PR"};
const STATUS_COLOR_C = {draft: "#94a3b8", submitted: "#3b82f6", acknowledged: "#f59e0b"};
if (thirdParty.length === 0) {
display(html`<p class="dim">No open outbound contributions. ✓</p>`);
} else {
display(html`<div class="task-list">${thirdParty.map(c => html`
<div class="task-item">
<div class="task-item-header">
<span class="task-badge" style="background:#f1f5f9;color:#475569">${TYPE_LABEL[c.type] ?? c.type}</span>
<span class="task-status-chip" style="background:${STATUS_COLOR_C[c.status] ?? '#ccc'}20;color:${STATUS_COLOR_C[c.status] ?? '#666'}">${c.status}</span>
${c.target_org ? html`<span class="task-context">${c.target_org}</span>` : ""}
${c.target_repo ? html`<span class="task-context task-ws-name">${c.target_repo}</span>` : ""}
</div>
<div class="task-title">${c.title}</div>
${c.body_path ? html`<div class="task-blocking-reason" style="background:#f0fdf4;color:#166534">↗ ${c.body_path}</div>` : ""}
</div>
`)}</div>`);
}
```
<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; }
/* ── Task list ────────────────────────────────────────────────────────────── */
.task-list { display: flex; flex-direction: column; gap: 0.5rem; }
.task-item { border-left: 3px solid var(--theme-foreground-faint, #ccc); border-radius: 0 6px 6px 0; background: var(--theme-background-alt); padding: 0.65rem 0.9rem; }
.task-item.status-blocked { border-left-color: #ef4444; }
.task-item.status-in_progress { border-left-color: #3b82f6; }
.task-item.status-todo { border-left-color: #94a3b8; }
.task-item-header { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-bottom: 0.3rem; font-size: 0.75rem; }
.task-badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.task-priority-critical { background: #fee2e2; color: #991b1b; }
.task-priority-high { background: #ffedd5; color: #9a3412; }
.task-priority-medium { background: #dbeafe; color: #1e40af; }
.task-priority-low { background: #f1f5f9; color: #475569; }
.task-status-chip { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 500; }
.status-chip-blocked { background: #fee2e2; color: #991b1b; }
.status-chip-in_progress { background: #dbeafe; color: #1e40af; }
.status-chip-todo { background: #f1f5f9; color: #475569; }
.task-context { color: var(--theme-foreground-muted, #666); }
.task-ws-name { font-style: italic; }
.task-assignee { color: var(--theme-foreground-muted, #888); }
.task-title { font-weight: 600; font-size: 0.95rem; margin-bottom: 0.15rem; }
.task-blocking-reason { font-size: 0.8rem; color: #b45309; background: #fef3c7; border-radius: 4px; padding: 0.2rem 0.5rem; margin-top: 0.25rem; }
.dim { color: gray; font-style: italic; }
</style>