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

@@ -103,8 +103,9 @@ function fmtDuration(ms) {
# Decisions
```js
import {withDocHelp} from "./components/doc-overlay.js";
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
import {injectTocTop} from "./components/toc-sidebar.js";
import {openActionConfirm} from "./components/action-confirm.js";
// ── KPI computation (uses full data, not filtered) ──────────────────────────
const _resolved5 = data
@@ -331,6 +332,24 @@ if (filtered.length === 0) {
const _ageText = d.decided_at ? `took ${fmtDuration(_ageMs)}` : `open ${fmtDuration(_ageMs)}`;
const _ageWarn = _isOpen && _meanResolveMs !== null && _ageMs > _meanResolveMs;
function onResolve() {
openActionConfirm({
title: "Resolve Decision",
entityTitle: d.title,
label: "Rationale",
placeholder: "Why is this resolved, and what was decided?",
confirmLabel: "Resolve",
onConfirm: async (rationale) => {
const res = await fetch(`${API}/decisions/${d.id}/`, {
method: "PATCH",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({status: "resolved", rationale, decided_by: "human"}),
});
if (!res.ok) throw new Error(`API error ${res.status}`);
},
});
}
return html`<div class="dec-item" style="border-left-color:${border}">
<div class="dec-item-header">
<span class="dec-badge ${TYPE_CLASS[d.decision_type] ?? ''}">${d.decision_type}</span>
@@ -343,6 +362,7 @@ if (filtered.length === 0) {
</span>` : ""}
<span class="dec-age ${_ageWarn ? 'dec-age-warn' : ''}">${_ageText}</span>
<span class="dec-date">${fmtDate(d.created_at)}</span>
${_isOpen ? html`<button class="dec-resolve-btn" onclick=${onResolve}>Resolve</button>` : ""}
</div>
<div class="dec-title">${d.title}</div>
${snippet ? html`<div class="dec-snippet">${snippet}${snippet.length < (d.description || d.rationale || "").length ? "…" : ""}</div>` : ""}
@@ -453,6 +473,10 @@ if (escalated.length > 0) {
.dec-resolved-by { font-size: 0.78rem; color: #22c55e; margin-top: 0.3rem; }
.dec-escalation-note { font-size: 0.78rem; color: #b45309; margin-top: 0.3rem; background: #fef3c7; border-radius: 4px; padding: 0.25rem 0.5rem; }
/* ── Resolve button ───────────────────────────────────────────────────────── */
.dec-resolve-btn { margin-left: auto; padding: 0.15rem 0.6rem; border-radius: 6px; border: 1px solid #22c55e; background: #f0fdf4; color: #166534; font-size: 0.7rem; font-weight: 600; cursor: pointer; }
.dec-resolve-btn:hover { background: #dcfce7; }
/* ── Escalation warning ───────────────────────────────────────────────────── */
.escalation-box { background: #fff3cd; border: 2px solid orange; border-radius: 8px; padding: 1rem; margin-top: 1rem; }
</style>