Files
state-hub/dashboard/src/contributions.md
tegwick 90c5ea50f7 feat(dashboard): poll optimisation — T4, T5, T6
T4: workstreams.md and dependencies.md now call /state/deps instead of the
    full /state/summary — removes 2 heavy 10-table queries per 60 s cycle.

T5: index.md's 4 independent polling loops (summaryState, sbomSnapState,
    regsState, wsChartState) consolidated into a single pageState generator
    with one Promise.all batch and a shared backoff counter.

T6: config.js gains waitForVisible(ms) — pauses polling entirely while the
    tab is hidden and fires immediately on visibilitychange.  pollDelay()
    simplified (hidden-tab POLL_HIDDEN logic removed).  All 16 polling pages
    migrated from await sleep(pollDelay(...)) to await waitForVisible(pollDelay(...)).

CUST-WP-0039 complete — all 6 tasks done.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 17:58:18 +02:00

6.7 KiB

title
title
Contributions
import {API, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
const POLL = 30_000;
// Live poll for contributions
const contribState = (async function*() {
  let failures = 0;
  while (true) {
    let data = [], ok = false;
    try {
      const r = await apiFetch("/contributions/");
      ok = r.ok;
      data = ok ? await r.json() : [];
    } catch {}
    failures = ok ? 0 : failures + 1;
    yield {data, ok, ts: new Date()};
    await waitForVisible(pollDelay({ok, base: POLL, failures}));
  }
})();
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}));
<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>