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>
This commit is contained in:
2026-05-11 17:58:18 +02:00
parent b832032cc3
commit 90c5ea50f7
18 changed files with 111 additions and 155 deletions

View File

@@ -3,117 +3,39 @@ title: Overview
---
```js
import {API, POLL, POLL_HEAVY, apiFetch, pollDelay, sleep} from "./components/config.js";
import {API, POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
```
```js
// Live polling — yields {data, ok, ts}; backs off when the API is slow/offline.
const summaryState = (async function*() {
// Single polling loop — fetches all data in one Promise.all batch, backs off uniformly.
const pageState = (async function*() {
let failures = 0;
while (true) {
let data, ok = false;
let summary = {}, snapshots = [], totalPkgs = 0, milestones = [], wsAll = [], ok = false;
try {
const r = await apiFetch("/state/summary", {timeout: 20_000});
ok = r.ok;
data = ok ? await r.json() : {error: `HTTP ${r.status}`};
} catch (e) {
data = {error: "API unreachable"};
}
failures = ok ? 0 : failures + 1;
yield {data, ok, ts: new Date()};
await sleep(pollDelay({ok, base: POLL_HEAVY, failures}));
}
})();
```
```js
const summary = summaryState.data ?? {};
const _ok = summaryState.ok ?? false;
const _ts = summaryState.ts;
const totals = summary.totals ?? {};
const ws = totals.workstreams ?? {};
const tasks = totals.tasks ?? {};
const decisions = totals.decisions ?? {};
```
```js
// Blocking decisions — fetched once on load, refreshed only after a resolve action.
// Kept separate from the summary poll so in-progress form inputs aren't wiped every 15 s.
const blockingDecisions = Mutable([]);
const refreshDecisions = async () => {
const r = await fetch(`${API}/decisions/?decision_type=pending`).catch(() => null);
const all = r?.ok ? await r.json() : [];
blockingDecisions.value = all.filter(d => ["open", "escalated"].includes(d.status));
};
refreshDecisions();
```
```js
// SBOM snapshots — repo coverage and total package count
const sbomSnapState = (async function*() {
let failures = 0;
while (true) {
let snapshots = [], totalPkgs = 0, ok = false;
try {
const r = await apiFetch("/sbom/snapshots/");
ok = r.ok;
if (r.ok) {
snapshots = await r.json();
totalPkgs = snapshots.reduce((s, sn) => s + (sn.entry_count ?? 0), 0);
}
} catch {}
failures = ok ? 0 : failures + 1;
yield {snapshots, totalPkgs};
await sleep(pollDelay({ok, base: POLL_HEAVY, failures}));
}
})();
```
```js
// Registered projects — milestone events tagged with registration
const regsState = (async function*() {
let failures = 0;
while (true) {
let rows = [], ok = false;
try {
const r = await apiFetch("/progress/?event_type=milestone&limit=500");
ok = r.ok;
if (r.ok) {
const all = await r.json();
rows = all.filter(e => e.summary?.startsWith("Project registered with State Hub:"));
}
} catch {}
failures = ok ? 0 : failures + 1;
yield rows;
await sleep(pollDelay({ok, base: POLL_HEAVY, failures}));
}
})();
```
```js
// All-workstreams + all-tasks poll — drives the multi-mode chart
const wsChartState = (async function*() {
let failures = 0;
while (true) {
let wsAll = [], ok = false;
try {
const [rw, rt, rto, rr, rwi] = await Promise.all([
const [rSum, rSnap, rRegs, rw, rt, rto, rr, rwi] = await Promise.all([
apiFetch("/state/summary", {timeout: 20_000}),
apiFetch("/sbom/snapshots/"),
apiFetch("/progress/?event_type=milestone&limit=500"),
apiFetch("/workstreams/"),
apiFetch("/tasks/?limit=2000"),
apiFetch("/topics/"),
apiFetch("/repos/"),
apiFetch("/workstreams/workplan-index"),
]);
ok = rw.ok && rt.ok && rto.ok && rr.ok;
ok = rSum.ok && rSnap.ok && rRegs.ok && rw.ok && rt.ok && rto.ok && rr.ok;
if (ok) {
const [wsList, taskList, topicList, repoList] = await Promise.all([
rw.json(), rt.json(), rto.json(), rr.json(),
const [summaryData, snapList, allEvents, wsList, taskList, topicList, repoList] = await Promise.all([
rSum.json(), rSnap.json(), rRegs.json(), rw.json(), rt.json(), rto.json(), rr.json(),
]);
summary = summaryData;
snapshots = snapList;
totalPkgs = snapshots.reduce((s, sn) => s + (sn.entry_count ?? 0), 0);
milestones = allEvents.filter(e => e.summary?.startsWith("Project registered with State Hub:"));
const workplanIndex = rwi.ok ? await rwi.json() : {workstreams: {}};
const workplanMap = workplanIndex.workstreams ?? {};
const topicMap = Object.fromEntries(topicList.map(t => [t.id, t]));
const repoMap = Object.fromEntries(repoList.map(r => [r.id, r]));
// Aggregate task counts per workstream
const counts = {};
for (const t of taskList) {
const wid = t.workstream_id;
@@ -139,17 +61,40 @@ const wsChartState = (async function*() {
...(counts[w.id] ?? {done: 0, in_progress: 0, blocked: 0, todo: 0, total: 0}),
};
});
} else {
summary = {error: "API unreachable"};
}
} catch {}
} catch (e) {
summary = {error: "API unreachable"};
}
failures = ok ? 0 : failures + 1;
yield {wsAll, ok};
await sleep(pollDelay({ok, base: POLL_HEAVY, failures}));
yield {summary, snapshots, totalPkgs, milestones, wsAll, ok, ts: new Date()};
await waitForVisible(pollDelay({ok, base: POLL_HEAVY, failures}));
}
})();
```
```js
const wsAll = wsChartState.wsAll ?? [];
const summary = pageState.summary ?? {};
const _ok = pageState.ok ?? false;
const _ts = pageState.ts;
const totals = summary.totals ?? {};
const ws = totals.workstreams ?? {};
const tasks = totals.tasks ?? {};
const decisions = totals.decisions ?? {};
const wsAll = pageState.wsAll ?? [];
```
```js
// Blocking decisions — fetched once on load, refreshed only after a resolve action.
// Kept separate from the main poll so in-progress form inputs aren't wiped every 60 s.
const blockingDecisions = Mutable([]);
const refreshDecisions = async () => {
const r = await fetch(`${API}/decisions/?decision_type=pending`).catch(() => null);
const all = r?.ok ? await r.json() : [];
blockingDecisions.value = all.filter(d => ["open", "escalated"].includes(d.status));
};
refreshDecisions();
```
# Custodian State Hub
@@ -390,8 +335,8 @@ const contribCounts = summary.contribution_counts ?? {};
const licenceRisk = summary.licence_risk_count ?? 0;
const totalContribs = ["br","fr","ep","upr"].reduce((s, t) => s + (contribCounts[t] ?? 0), 0);
const needsFollowUp = (contribCounts["submitted"] ?? 0) + (contribCounts["acknowledged"] ?? 0);
const sbomSnaps = sbomSnapState.snapshots ?? [];
const totalPkgs = sbomSnapState.totalPkgs ?? 0;
const sbomSnaps = pageState.snapshots ?? [];
const totalPkgs = pageState.totalPkgs ?? 0;
display(html`<div class="grid grid-cols-3" style="gap:1rem;margin-bottom:1.5rem">
<a class="card card-link" href="./contributions">
@@ -508,7 +453,7 @@ if (nextSteps.length === 0) {
## Registered Projects
```js
const regs = regsState ?? [];
const regs = pageState.milestones ?? [];
if (regs.length === 0) {
display(html`<p style="color:gray">No projects registered yet. Run <code>custodian register-project</code> inside a repo.</p>`);
} else {
@@ -523,7 +468,7 @@ if (regs.length === 0) {
```js
// Registered domains with no workstreams yet — show a getting-started hint
const regs = regsState ?? [];
const regs = pageState.milestones ?? [];
const registeredDomains = new Set(regs.map(e => e.detail?.domain).filter(Boolean));
const emptyRegistered = (summary.topics ?? []).filter(t =>
registeredDomains.has(t.domain_slug) && (t.workstreams ?? []).length === 0