generated from coulomb/repo-seed
EP catalogue (all domains): - EP-RAIL-001 ep_id patched (schema fix: add ep_id to EPUpdate) - EP-RAIL-003 (git bare-repo mirrors) and EP-RAIL-004 (offsite secondary backup) registered from railiance-cluster/docs/backup-restore.md - EP-CUST-003..007 ep_ids assigned to existing custodian EPs - EP-CUST-008 (State Hub API auth) and EP-CUST-009 (update_workstream MCP tool) registered as new custodian extension points TD catalogue (railiance — first 5 items): - TD-RAIL-001: backup cron runs as root without audit trail (high/security) - TD-RAIL-002: k3s kubeconfig world-readable mode 644 (medium/security) - TD-RAIL-003: no Ansible role unit tests (medium/test) - TD-RAIL-004: age key extracted via awk — fragile (medium/impl) - TD-RAIL-005: etcd snapshot retention uncoordinated (low/impl) Dashboard (T08 + T10): - Extract API URL and POLL to src/components/config.js; all 15 pages now import from the shared module (contributions/goals keep custom POLL) - Shared .kpi-infobox, .filter-bar, .filter-search/.filter-owner CSS moved to observablehq.config.js head <style> block; removed from 9 pages - Build: 0 errors, 0 warnings API (T09): - progress.py: limit param now Query(100, le=1000) — prevents unbounded list requests; closes TD-CUST-004 for the only endpoint that had limit CUST-WP-0004 marked completed (all 10 tasks done). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.6 KiB
6.6 KiB
title
| title |
|---|
| Contributions |
import {API} from "./components/config.js";
const POLL = 30_000;
// 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));
}
})();
const contribs = contribState.data ?? [];
const _ok = contribState.ok ?? false;
const _ts = contribState.ts;
Contributions
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"); }
// 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>`);
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
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
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
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}));