Dashboard decisions: stable form inputs + copy to clipboard
Polling fix: - Blocking decisions now use a Mutable (blockingDecisions) + refreshDecisions() instead of deriving from summary.blocking_decisions - Cards only re-render on initial page load or after a successful resolve, not on every 15 s summary poll — so typing in the form is never interrupted - On successful resolve, refreshDecisions() re-fetches the list; the resolved decision no longer matches the open/escalated filter so it disappears cleanly Copy to clipboard: - Small "Copy" button in the card header (next to deadline/escalation badges) - Formats full decision as markdown: title, description, context, status, date - Shows "✓ Copied" for 1.5 s, reverts to "Copy"; shows "⚠ Failed" on error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,18 @@ const tasks = totals.tasks ?? {};
|
|||||||
const decisions = totals.decisions ?? {};
|
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
|
```js
|
||||||
// Registered projects — milestone events tagged with registration
|
// Registered projects — milestone events tagged with registration
|
||||||
const regsState = (async function*() {
|
const regsState = (async function*() {
|
||||||
@@ -205,7 +217,9 @@ if (emptyRegistered.length > 0) {
|
|||||||
## Blocking Decisions
|
## Blocking Decisions
|
||||||
|
|
||||||
```js
|
```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) {
|
if (blocking.length === 0) {
|
||||||
display(html`<p style="color:green">✓ No blocking decisions.</p>`);
|
display(html`<p style="color:green">✓ No blocking decisions.</p>`);
|
||||||
} else {
|
} else {
|
||||||
@@ -216,6 +230,7 @@ if (blocking.length === 0) {
|
|||||||
<span class="dec-meta">
|
<span class="dec-meta">
|
||||||
${d.escalation_note ? html`<span class="dec-warn-badge">⚠ escalated</span>` : ""}
|
${d.escalation_note ? html`<span class="dec-warn-badge">⚠ escalated</span>` : ""}
|
||||||
${d.deadline ? html`<span>Due ${new Date(d.deadline).toLocaleDateString()}</span>` : ""}
|
${d.deadline ? html`<span>Due ${new Date(d.deadline).toLocaleDateString()}</span>` : ""}
|
||||||
|
<button class="r-copy" title="Copy decision to clipboard">Copy</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
${d.description ? html`<p class="dec-desc">${d.description}</p>` : ""}
|
${d.description ? html`<p class="dec-desc">${d.description}</p>` : ""}
|
||||||
@@ -236,13 +251,31 @@ if (blocking.length === 0) {
|
|||||||
</details>
|
</details>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const btn = card.querySelector(".r-submit");
|
// Copy to clipboard
|
||||||
const msg = card.querySelector(".r-msg");
|
const copyBtn = card.querySelector(".r-copy");
|
||||||
const det = card.querySelector(".dec-resolve");
|
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 () => {
|
btn.addEventListener("click", async () => {
|
||||||
const rationale = card.querySelector(".r-text").value.trim();
|
const rationale = card.querySelector(".r-text").value.trim();
|
||||||
const decidedBy = card.querySelector(".r-by").value.trim() || "Bernd";
|
const decidedBy = card.querySelector(".r-by").value.trim() || "Bernd";
|
||||||
if (!rationale) { msg.textContent = "⚠ Please enter a rationale."; return; }
|
if (!rationale) { msg.textContent = "⚠ Please enter a rationale."; return; }
|
||||||
btn.disabled = true; btn.textContent = "Saving…";
|
btn.disabled = true; btn.textContent = "Saving…";
|
||||||
try {
|
try {
|
||||||
@@ -252,10 +285,7 @@ if (blocking.length === 0) {
|
|||||||
body: JSON.stringify({rationale, decided_by: decidedBy}),
|
body: JSON.stringify({rationale, decided_by: decidedBy}),
|
||||||
});
|
});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
det.open = false;
|
await refreshDecisions(); // re-fetches list — resolved decision won't appear
|
||||||
det.querySelector("summary").textContent = "✓ Resolved — DECISIONS.md written, updates in next poll";
|
|
||||||
det.querySelector("summary").style.color = "green";
|
|
||||||
card.style.opacity = "0.55";
|
|
||||||
} else {
|
} else {
|
||||||
const err = await r.json().catch(() => ({}));
|
const err = await r.json().catch(() => ({}));
|
||||||
msg.textContent = `Error ${r.status}: ${err.detail ?? "unknown"}`;
|
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 { 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; }
|
.dec-resolve-actions button:disabled { opacity: 0.5; cursor: default; }
|
||||||
.r-msg { font-size: 0.8rem; color: #b45309; }
|
.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); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user