generated from coulomb/repo-seed
feat(state-hub): v0.3 MCP tools + dashboard pages for contributions and SBOM
MCP server additions (5 tools + 3 resources):
- register_contribution(), update_contribution_status(), get_contributions()
- ingest_sbom_tool(repo_slug, lockfile_path) — shells out to ingest_sbom.py
- get_licence_report()
- state://contributions, state://sbom/aggregated, state://sbom/{repo_slug}
Dashboard pages:
- contributions.md — live-polled Kanban by status (draft→merged), filter bar
(type/status/repo), KPI grid (total + per type), follow-up banner, full table
- sbom.md — licence distribution bar chart (Plot), copyleft risk section,
package table with ecosystem/direct/dev filters, repo-slug resolution
- data/contributions.json.py, data/sbom.json.py — Observable data loaders
- index.md — added Contribution & SBOM Health KPI row (total, follow-up count,
copyleft risk indicator; sourced from state summary fields)
- observablehq.config.js — added Contributions + SBOM to nav
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
166
dashboard/src/contributions.md
Normal file
166
dashboard/src/contributions.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: Contributions
|
||||
---
|
||||
|
||||
```js
|
||||
const API = "http://127.0.0.1:8000";
|
||||
const POLL = 30_000;
|
||||
```
|
||||
|
||||
```js
|
||||
// Live poll for contributions
|
||||
const contribState = (async function*() {
|
||||
while (true) {
|
||||
let data = [], ok = false;
|
||||
try {
|
||||
const r = await fetch(`${API}/contributions/`);
|
||||
ok = r.ok;
|
||||
data = ok ? await r.json() : [];
|
||||
} catch {}
|
||||
yield {data, ok, ts: new Date()};
|
||||
await new Promise(res => setTimeout(res, POLL));
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
```js
|
||||
const contribs = contribState.data ?? [];
|
||||
const _ok = contribState.ok ?? false;
|
||||
const _ts = contribState.ts;
|
||||
```
|
||||
|
||||
# Contributions
|
||||
|
||||
```js
|
||||
import {injectTocTop} from "./components/toc-sidebar.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>`;
|
||||
injectTocTop("live-indicator", _liveEl);
|
||||
```
|
||||
|
||||
```js
|
||||
// Filters
|
||||
const typeFilter = Inputs.select(["all", "br", "fr", "ep", "upr"], {label: "Type", value: "all"});
|
||||
const statFilter = Inputs.select(
|
||||
["all", "draft", "submitted", "acknowledged", "accepted", "rejected", "merged", "withdrawn"],
|
||||
{label: "Status", value: "all"}
|
||||
);
|
||||
const repoFilter = Inputs.text({label: "Target repo", placeholder: "filter by repo…"});
|
||||
display(html`<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem">
|
||||
${typeFilter}${statFilter}${repoFilter}
|
||||
</div>`);
|
||||
```
|
||||
|
||||
```js
|
||||
const tf = typeFilter.value;
|
||||
const sf = statFilter.value;
|
||||
const rf = repoFilter.value?.trim().toLowerCase() ?? "";
|
||||
|
||||
const filtered = contribs.filter(c =>
|
||||
(tf === "all" || c.type === tf) &&
|
||||
(sf === "all" || c.status === sf) &&
|
||||
(!rf || (c.target_repo ?? "").toLowerCase().includes(rf))
|
||||
);
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
```js
|
||||
const typeLabels = {br: "Bug Report", fr: "Feature Request", ep: "Extension Point", upr: "Upstream PR"};
|
||||
const typeCounts = Object.fromEntries(["br","fr","ep","upr"].map(t => [
|
||||
t, contribs.filter(c => c.type === t).length
|
||||
]));
|
||||
const needsFollowUp = contribs.filter(c => ["submitted","acknowledged"].includes(c.status)).length;
|
||||
|
||||
display(html`<div class="grid grid-cols-5" style="gap:1rem;margin-bottom:1.5rem">
|
||||
<div class="card">
|
||||
<h3>Total</h3>
|
||||
<p class="big-num">${contribs.length}</p>
|
||||
</div>
|
||||
${["br","fr","ep","upr"].map(t => html`
|
||||
<div class="card">
|
||||
<h3>${typeLabels[t]}</h3>
|
||||
<p class="big-num">${typeCounts[t]}</p>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
${needsFollowUp > 0 ? html`<div class="follow-up-banner">⚠ ${needsFollowUp} contribution(s) awaiting upstream response (submitted / acknowledged)</div>` : ""}
|
||||
`);
|
||||
```
|
||||
|
||||
## Status Kanban
|
||||
|
||||
```js
|
||||
const statusCols = [
|
||||
{key: "draft", label: "Draft", color: "#aaa"},
|
||||
{key: "submitted", label: "Submitted", color: "steelblue"},
|
||||
{key: "acknowledged", label: "Acknowledged",color: "#f0a500"},
|
||||
{key: "accepted", label: "Accepted", color: "#4caf50"},
|
||||
{key: "merged", label: "Merged", color: "#2e7d32"},
|
||||
{key: "rejected", label: "Rejected", color: "#e53935"},
|
||||
{key: "withdrawn", label: "Withdrawn", color: "#bbb"},
|
||||
];
|
||||
|
||||
const colMap = {};
|
||||
for (const c of filtered) {
|
||||
(colMap[c.status] = colMap[c.status] ?? []).push(c);
|
||||
}
|
||||
|
||||
const activeCols = statusCols.filter(s => colMap[s.key]?.length);
|
||||
if (activeCols.length === 0) {
|
||||
display(html`<p style="color:gray">No contributions match the current filters.</p>`);
|
||||
} else {
|
||||
display(html`<div class="kanban">
|
||||
${activeCols.map(s => html`
|
||||
<div class="kanban-col">
|
||||
<div class="kanban-header" style="border-bottom:2px solid ${s.color}">${s.label} <span class="kanban-count">${colMap[s.key].length}</span></div>
|
||||
${colMap[s.key].map(c => html`
|
||||
<div class="contrib-card">
|
||||
<div class="contrib-badge contrib-badge-${c.type}">${c.type.toUpperCase()}</div>
|
||||
<div class="contrib-title">${c.title}</div>
|
||||
${c.target_org || c.target_repo ? html`<div class="contrib-repo">${[c.target_org, c.target_repo].filter(Boolean).join("/")}</div>` : ""}
|
||||
${c.body_path ? html`<div class="contrib-path">${c.body_path}</div>` : ""}
|
||||
<div class="contrib-date">${new Date(c.created_at).toLocaleDateString()}</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`)}
|
||||
</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
## All Contributions
|
||||
|
||||
```js
|
||||
display(Inputs.table(filtered.map(c => ({
|
||||
Type: c.type.toUpperCase(),
|
||||
Title: c.title,
|
||||
Status: c.status,
|
||||
Target: [c.target_org, c.target_repo].filter(Boolean).join("/") || "—",
|
||||
Created: new Date(c.created_at).toLocaleDateString(),
|
||||
})), {maxWidth: 900}));
|
||||
```
|
||||
|
||||
<style>
|
||||
.live-indicator { font-size: 0.8rem; color: gray; padding: 0.55rem 0.7rem; margin-bottom: 0.75rem; }
|
||||
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
|
||||
.big-num { font-size: 2.2rem; font-weight: bold; margin: 0.25rem 0; }
|
||||
.follow-up-banner { background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 0.5rem 0.9rem; margin-bottom: 1rem; font-size: 0.9rem; }
|
||||
.kanban { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
||||
.kanban-col { background: var(--theme-background-alt); border-radius: 8px; padding: 0.75rem; }
|
||||
.kanban-header { font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; padding-bottom: 0.5rem; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.4rem; }
|
||||
.kanban-count { font-size: 0.75rem; background: var(--theme-background); border-radius: 10px; padding: 0.1rem 0.4rem; font-weight: 500; }
|
||||
.contrib-card { background: var(--theme-background); border-radius: 6px; padding: 0.6rem 0.75rem; margin-bottom: 0.5rem; }
|
||||
.contrib-badge { display: inline-block; font-size: 0.65rem; font-weight: 700; border-radius: 3px; padding: 0.1rem 0.35rem; margin-bottom: 0.25rem; }
|
||||
.contrib-badge-br { background: #fde8e8; color: #c62828; }
|
||||
.contrib-badge-fr { background: #e3f2fd; color: #1565c0; }
|
||||
.contrib-badge-ep { background: #f3e5f5; color: #6a1b9a; }
|
||||
.contrib-badge-upr { background: #e8f5e9; color: #2e7d32; }
|
||||
.contrib-title { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.2rem; line-height: 1.3; }
|
||||
.contrib-repo { font-size: 0.75rem; color: steelblue; font-family: monospace; }
|
||||
.contrib-path { font-size: 0.7rem; color: gray; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.contrib-date { font-size: 0.7rem; color: gray; margin-top: 0.3rem; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user