state-hub/dashboard/src/suggestions.md
tegwick f2e042a278 Finish CUST-WP-0011 and implement STATE-WP-0061 suggestion backlog.
Add persisted Suggestion entities with WSJF ranking, relevance bumps,
REST/MCP write surfaces, /suggestions dashboard page, and daily triage
digest integration. Document the cluster operating model, archive the
completed migration workplan, and seed WARDEN-WP-0012 routing scenarios.
2026-07-06 10:52:49 +02:00

2.7 KiB
Raw Blame History

title
Suggestions
import {apiFetch, pollDelay, waitForVisible} from "./components/config.js";
const POLL = 30_000;
const sugState = (async function*() {
  let failures = 0;
  while (true) {
    let data = [], ok = false;
    try {
      const r = await apiFetch("/suggestions/?rank=wsjf&limit=100");
      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 suggestions = sugState.data ?? [];
const _ok = sugState.ok ?? false;
const _ts = sugState.ts;

Demand-Weighted Suggestions

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/suggestions"); }

display(html`<p class="dim">Ranked by WSJF = (base_value + relevance_weight × relevance) / job_size. Gated needs accrue relevance when unmet.</p>`);
display(html`<p class="dim"><a href="/wsjf-triage">Daily WSJF triage</a> consumes this backlog in its digest.</p>`);
const stageBadge = (stage) => {
  const colors = {
    suggestion: "#6b7280",
    requirement: "#2563eb",
    promoted: "#059669",
    declined: "#9ca3af",
  };
  return html`<span style="font-size:0.75rem;padding:0.1rem 0.45rem;border-radius:4px;background:${colors[stage] ?? "#ccc'};color:#fff">${stage}</span>`;
};

const rows = suggestions.map((s) => html`<tr>
  <td>${stageBadge(s.stage)}</td>
  <td><strong>${s.title}</strong>${s.origin_ref ? html`<br/><code style="font-size:0.75rem">${s.origin_ref}</code>` : ""}</td>
  <td>${s.domain_slug || "—"}</td>
  <td style="text-align:right">${s.relevance}</td>
  <td style="text-align:right">${s.wsjf?.toFixed?.(1) ?? s.wsjf}</td>
  <td style="font-size:0.8rem">${s.last_requested_at ? new Date(s.last_requested_at).toLocaleString() : "—"}</td>
</tr>`);

display(html`<table class="dashboard-table" style="width:100%;font-size:0.88rem">
  <thead><tr>
    <th>Stage</th><th>Title</th><th>Domain</th><th>Relevance</th><th>WSJF</th><th>Last requested</th>
  </tr></thead>
  <tbody>${rows.length ? rows : html`<tr><td colspan="6" class="dim">No open suggestions yet.</td></tr>`}</tbody>
</table>`);