diff --git a/state-hub/dashboard/src/index.md b/state-hub/dashboard/src/index.md index a289d50..16fe14b 100644 --- a/state-hub/dashboard/src/index.md +++ b/state-hub/dashboard/src/index.md @@ -35,6 +35,18 @@ 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 // Registered projects — milestone events tagged with registration const regsState = (async function*() { @@ -205,7 +217,9 @@ if (emptyRegistered.length > 0) { ## Blocking Decisions ```js -const blocking = summary.blocking_decisions ?? []; +// Uses blockingDecisions (Mutable) — only re-renders when refreshDecisions() is called, +// not on every summary poll, so in-progress form input is preserved between polls. +const blocking = blockingDecisions ?? []; if (blocking.length === 0) { display(html`

✓ No blocking decisions.

`); } else { @@ -216,6 +230,7 @@ if (blocking.length === 0) { ${d.escalation_note ? html`⚠ escalated` : ""} ${d.deadline ? html`Due ${new Date(d.deadline).toLocaleDateString()}` : ""} + ${d.description ? html`

${d.description}

` : ""} @@ -236,13 +251,31 @@ if (blocking.length === 0) { `; - const btn = card.querySelector(".r-submit"); - const msg = card.querySelector(".r-msg"); - const det = card.querySelector(".dec-resolve"); + // Copy to clipboard + const copyBtn = card.querySelector(".r-copy"); + copyBtn.addEventListener("click", () => { + const parts = [ + `# ${d.title}`, + "", + d.description ?? "", + d.rationale ? `\n**Context:** ${d.rationale}` : "", + d.escalation_note ? `\n**⚠ Escalated:** ${d.escalation_note}` : "", + `\n**Status:** ${d.status} | **Created:** ${new Date(d.created_at).toLocaleDateString()}`, + d.deadline ? `**Due:** ${new Date(d.deadline).toLocaleDateString()}` : "", + ].filter(Boolean).join("\n"); + navigator.clipboard.writeText(parts).then(() => { + copyBtn.textContent = "✓ Copied"; + setTimeout(() => { copyBtn.textContent = "Copy"; }, 1500); + }).catch(() => { copyBtn.textContent = "⚠ Failed"; setTimeout(() => { copyBtn.textContent = "Copy"; }, 2000); }); + }); + + // Resolve + const btn = card.querySelector(".r-submit"); + const msg = card.querySelector(".r-msg"); btn.addEventListener("click", async () => { - const rationale = card.querySelector(".r-text").value.trim(); - const decidedBy = card.querySelector(".r-by").value.trim() || "Bernd"; + const rationale = card.querySelector(".r-text").value.trim(); + const decidedBy = card.querySelector(".r-by").value.trim() || "Bernd"; if (!rationale) { msg.textContent = "⚠ Please enter a rationale."; return; } btn.disabled = true; btn.textContent = "Saving…"; try { @@ -252,10 +285,7 @@ if (blocking.length === 0) { body: JSON.stringify({rationale, decided_by: decidedBy}), }); if (r.ok) { - det.open = false; - det.querySelector("summary").textContent = "✓ Resolved — DECISIONS.md written, updates in next poll"; - det.querySelector("summary").style.color = "green"; - card.style.opacity = "0.55"; + await refreshDecisions(); // re-fetches list — resolved decision won't appear } else { const err = await r.json().catch(() => ({})); msg.textContent = `Error ${r.status}: ${err.detail ?? "unknown"}`; @@ -326,4 +356,6 @@ display(Inputs.table((summary.recent_progress ?? []).map(e => ({ .dec-resolve-actions button { padding: 0.35rem 0.9rem; border-radius: 4px; border: none; background: steelblue; color: white; cursor: pointer; font-size: 0.875rem; } .dec-resolve-actions button:disabled { opacity: 0.5; cursor: default; } .r-msg { font-size: 0.8rem; color: #b45309; } +.r-copy { padding: 0.15rem 0.55rem; border-radius: 3px; border: 1px solid var(--theme-foreground-faint); background: var(--theme-background); color: var(--theme-foreground-muted); cursor: pointer; font-size: 0.75rem; } +.r-copy:hover { background: var(--theme-background-alt); }