diff --git a/dashboard/src/components/config.js b/dashboard/src/components/config.js
index 3499f94..d8f3c2d 100644
--- a/dashboard/src/components/config.js
+++ b/dashboard/src/components/config.js
@@ -28,10 +28,11 @@ export async function waitForVisible(ms) {
export async function apiFetch(path, options = {}) {
const url = path.startsWith("http") ? path : `${API}${path}`;
const timeout = options.timeout ?? FETCH_TIMEOUT;
+ const {timeout: _timeout, ...fetchOptions} = options;
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeout);
try {
- return await fetch(url, {...options, signal: ctrl.signal});
+ return await fetch(url, {cache: "no-store", ...fetchOptions, signal: ctrl.signal});
} finally {
clearTimeout(timer);
}
diff --git a/dashboard/src/index.md b/dashboard/src/index.md
index a32574b..bd729ef 100644
--- a/dashboard/src/index.md
+++ b/dashboard/src/index.md
@@ -20,61 +20,69 @@ const pageState = (async function*() {
while (true) {
let summary = {}, snapshots = [], totalPkgs = 0, milestones = [], wsAll = [], ok = false;
try {
- 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"),
+ const loadJson = async (name, path, options = {}) => {
+ const response = await apiFetch(path, options);
+ if (!response.ok) throw new Error(`${name} HTTP ${response.status}`);
+ return response.json();
+ };
+
+ const [
+ summaryData,
+ snapList,
+ allEvents,
+ wsList,
+ taskList,
+ topicList,
+ repoList,
+ workplanIndex,
+ ] = await Promise.all([
+ loadJson("summary", "/state/summary", {timeout: 20_000}),
+ loadJson("sbom snapshots", "/sbom/snapshots/"),
+ loadJson("milestones", "/progress/?event_type=milestone&limit=500"),
+ loadJson("workstreams", "/workstreams/"),
+ loadJson("tasks", "/tasks/?limit=2000"),
+ loadJson("topics", "/topics/"),
+ loadJson("repos", "/repos/"),
+ loadJson("workplan index", "/workstreams/workplan-index").catch(() => ({workstreams: {}})),
]);
- ok = rSum.ok && rSnap.ok && rRegs.ok && rw.ok && rt.ok && rto.ok && rr.ok;
- if (ok) {
- 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]));
- const counts = {};
- for (const t of taskList) {
- const wid = t.workstream_id;
- if (!counts[wid]) counts[wid] = {done: 0, in_progress: 0, blocked: 0, todo: 0, total: 0};
- counts[wid].total++;
- if (t.status === "done") counts[wid].done++;
- else if (t.status === "in_progress") counts[wid].in_progress++;
- else if (t.status === "blocked") counts[wid].blocked++;
- else if (t.status === "todo") counts[wid].todo++;
- }
- wsAll = wsList.map(w => {
- const repo = repoMap[w.repo_id];
- const topic = topicMap[w.topic_id];
- const workplan = workplanMap[w.id] ?? {};
- return {
- ...w,
- status: normalizeWorkstreamStatus(w.status),
- domain: repo?.domain_slug ?? topic?.domain_slug ?? "unknown",
- repo_label: repo?.slug ?? workplan.repo_slug ?? "unassigned",
- workplan_filename: workplan.filename ?? null,
- workplan_relative_path: workplan.relative_path ?? null,
- workplan_archived: workplan.archived ?? false,
- health_labels: workplan.health_labels ?? [],
- href: `./workstreams/${w.id}`,
- ...(counts[w.id] ?? {done: 0, in_progress: 0, blocked: 0, todo: 0, total: 0}),
- };
- });
- } else {
- summary = {error: "API unreachable"};
+
+ ok = true;
+ 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 workplanMap = workplanIndex.workstreams ?? {};
+ const topicMap = Object.fromEntries(topicList.map(t => [t.id, t]));
+ const repoMap = Object.fromEntries(repoList.map(r => [r.id, r]));
+ const counts = {};
+ for (const t of taskList) {
+ const wid = t.workstream_id;
+ if (!counts[wid]) counts[wid] = {done: 0, in_progress: 0, blocked: 0, todo: 0, total: 0};
+ counts[wid].total++;
+ if (t.status === "done") counts[wid].done++;
+ else if (t.status === "in_progress") counts[wid].in_progress++;
+ else if (t.status === "blocked") counts[wid].blocked++;
+ else if (t.status === "todo") counts[wid].todo++;
}
+ wsAll = wsList.map(w => {
+ const repo = repoMap[w.repo_id];
+ const topic = topicMap[w.topic_id];
+ const workplan = workplanMap[w.id] ?? {};
+ return {
+ ...w,
+ status: normalizeWorkstreamStatus(w.status),
+ domain: repo?.domain_slug ?? topic?.domain_slug ?? "unknown",
+ repo_label: repo?.slug ?? workplan.repo_slug ?? "unassigned",
+ workplan_filename: workplan.filename ?? null,
+ workplan_relative_path: workplan.relative_path ?? null,
+ workplan_archived: workplan.archived ?? false,
+ health_labels: workplan.health_labels ?? [],
+ href: `./workstreams/${w.id}`,
+ ...(counts[w.id] ?? {done: 0, in_progress: 0, blocked: 0, todo: 0, total: 0}),
+ };
+ });
} catch (e) {
- summary = {error: "API unreachable"};
+ summary = {error: `Dashboard data load failed: ${e?.message ?? String(e)}`};
}
failures = ok ? 0 : failures + 1;
yield {summary, snapshots, totalPkgs, milestones, wsAll, ok, ts: new Date()};
@@ -136,8 +144,8 @@ display(html`