feat(dashboard): Interventions page improvements and action-confirm modal

- Move Interventions under Workstreams in the navigator
- Add action-confirm.js: shared modal component for actions requiring a
  mandatory comment (survives live-poll re-renders, unlike inline DOM mutation)
- Wire action-confirm into Interventions (Mark done) and Decisions (Resolve)
- Fix Interventions completed section: fetch all tasks and filter client-side
  so resolved interventions (needs_human=false) still appear under Completed
- Add docs/interventions.md help page with ? button on the h1
- Replace all hardcoded "Bernd" with "human" across dashboard src and docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 23:15:06 +01:00
parent c792ab0bc0
commit 0bdf4929fc
9 changed files with 426 additions and 31 deletions

View File

@@ -8,13 +8,13 @@ const POLL = 15_000;
```
```js
// Live poll: human-flagged tasks + workstreams + topics
// Live poll: all tasks (filtered client-side) + workstreams + topics
const interventionState = (async function*() {
while (true) {
let tasks = [], wsMap = {}, ok = false;
try {
const [rt, rw, rto, rr] = await Promise.all([
fetch(`${API}/tasks/?needs_human=true`),
fetch(`${API}/tasks/?limit=500`),
fetch(`${API}/workstreams/`),
fetch(`${API}/topics/`),
fetch(`${API}/repos/`),
@@ -51,8 +51,10 @@ const _ts = interventionState.ts;
```js
const OPEN_STATUSES = new Set(["todo", "in_progress", "blocked"]);
const open = tasks.filter(t => OPEN_STATUSES.has(t.status));
const closed = tasks.filter(t => !OPEN_STATUSES.has(t.status));
// open = currently flagged for human action
// closed = previously flagged (intervention_note records the resolution comment)
const open = tasks.filter(t => t.needs_human === true);
const closed = tasks.filter(t => t.intervention_note && !OPEN_STATUSES.has(t.status) && !t.needs_human);
// Domain breakdown for top-3
const domainCounts = {};
@@ -107,15 +109,20 @@ withDocHelp(_liveEl, "/docs/live-data");
injectTocTop("intervention-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/interventions"); }
```
Tasks flagged `needs_human=true` — actions only Bernd can take.
Tasks flagged `needs_human=true` — actions only a human can take.
---
## Open
```js
import {openActionConfirm} from "./components/action-confirm.js";
const PRIORITY_ORDER = {critical: 0, high: 1, medium: 2, low: 3};
const STATUS_ORDER = {blocked: 0, in_progress: 1, todo: 2};
@@ -127,26 +134,35 @@ function sortTasks(arr) {
});
}
async function markDone(taskId) {
try {
await fetch(`${API}/tasks/${taskId}/`, {
method: "PATCH",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({status: "done", needs_human: false, intervention_note: null}),
});
} catch {}
}
function renderCard(t) {
const isOpen = OPEN_STATUSES.has(t.status);
const borderColor = isOpen ? "#f59e0b" : "#22c55e";
function onMarkDone() {
openActionConfirm({
title: "Mark Intervention as Done",
entityTitle: t.title,
label: "Resolution comment",
placeholder: "What was done to resolve this?",
confirmLabel: "Mark Done",
onConfirm: async (comment) => {
const res = await fetch(`${API}/tasks/${t.id}/`, {
method: "PATCH",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({status: "done", needs_human: false, intervention_note: comment}),
});
if (!res.ok) throw new Error(`API error ${res.status}`);
},
});
}
return html`<div class="intervention-card" style="border-left-color:${borderColor}">
<div class="int-card-header">
<span class="task-badge task-priority-${t.priority}">${t.priority}</span>
<span class="task-status-chip status-chip-${t.status}">${t.status.replace("_", " ")}</span>
<span class="task-context">${t.domain}</span>
<span class="task-context task-ws-name">${t.workstream_title}</span>
${isOpen ? html`<button class="done-btn" onclick=${() => markDone(t.id)}>Mark done</button>` : ""}
${isOpen ? html`<button class="done-btn" onclick=${onMarkDone}>Mark done</button>` : ""}
</div>
<div class="int-action">${t.intervention_note ?? "(no note)"}</div>
${t.title !== t.intervention_note ? html`<details class="int-desc"><summary>Task: ${t.title}</summary>${t.description ?? ""}</details>` : ""}