generated from coulomb/repo-seed
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:
@@ -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" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
158
dashboard/src/dependencies.md
Normal file
158
dashboard/src/dependencies.md
Normal 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>
|
||||
120
dashboard/src/docs/contributions.md
Normal file
120
dashboard/src/docs/contributions.md
Normal 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.*
|
||||
91
dashboard/src/docs/debt.md
Normal file
91
dashboard/src/docs/debt.md
Normal 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.
|
||||
90
dashboard/src/docs/dependencies.md
Normal file
90
dashboard/src/docs/dependencies.md
Normal 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**.
|
||||
82
dashboard/src/docs/domains.md
Normal file
82
dashboard/src/docs/domains.md
Normal 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.*
|
||||
102
dashboard/src/docs/extensions.md
Normal file
102
dashboard/src/docs/extensions.md
Normal 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.
|
||||
84
dashboard/src/docs/overview.md
Normal file
84
dashboard/src/docs/overview.md
Normal 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.
|
||||
119
dashboard/src/docs/reference.md
Normal file
119
dashboard/src/docs/reference.md
Normal 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
|
||||
83
dashboard/src/docs/repos.md
Normal file
83
dashboard/src/docs/repos.md
Normal 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.*
|
||||
95
dashboard/src/docs/tasks.md
Normal file
95
dashboard/src/docs/tasks.md
Normal 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/`
|
||||
71
dashboard/src/docs/todo.md
Normal file
71
dashboard/src/docs/todo.md
Normal 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.*
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
47
dashboard/src/reference.md
Normal file
47
dashboard/src/reference.md
Normal 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 |
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
235
dashboard/src/todo.md
Normal 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>
|
||||
Reference in New Issue
Block a user