Dashboard: make status cards interactive links

- Active Workstreams → navigates to ./workstreams page
- Blocking Decisions → anchor-scrolls to #blocking-decisions section
- Blocked Tasks → click toggles inline panel showing each blocked task
  with workstream name and blocking reason; label toggles expand/collapse
- Events Today → anchor-scrolls to #recent-activity section
- All cards get hover lift effect (box-shadow + 1px translateY)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 23:43:44 +01:00
parent f34b49ebde
commit da71a1bfac

View File

@@ -83,29 +83,60 @@ if (summary.error) display(html`<div class="warning">⚠️ ${summary.error}</di
## Status
```js
display(html`<div class="grid grid-cols-4" style="gap:1rem;margin-bottom:1.5rem">
<div class="card">
<h3>Active Workstreams</h3>
<p class="big-num">${ws.active ?? 0}</p>
<small>${ws.blocked ?? 0} blocked</small>
const blockedTasks = summary.blocked_tasks ?? [];
const wsById = Object.fromEntries((summary.open_workstreams ?? []).map(w => [w.id, w]));
const todayCount = (summary.recent_progress ?? []).filter(e =>
e.created_at?.startsWith(new Date().toISOString().slice(0, 10))).length;
const decCount = (decisions.open ?? 0) + (decisions.escalated ?? 0);
const statusEl = html`<div>
<div class="grid grid-cols-4" style="gap:1rem;margin-bottom:0.75rem">
<a class="card card-link" href="./workstreams">
<h3>Active Workstreams</h3>
<p class="big-num">${ws.active ?? 0}</p>
<small>${ws.blocked ?? 0} blocked</small>
</a>
<a class="card card-link ${decCount > 0 ? 'warn' : ''}" href="#blocking-decisions">
<h3>Blocking Decisions</h3>
<p class="big-num">${decCount}</p>
<small>${decisions.escalated ?? 0} escalated</small>
</a>
<div class="card card-link ${blockedTasks.length > 0 ? 'warn' : ''}" data-toggle="blocked-panel">
<h3>Blocked Tasks</h3>
<p class="big-num">${blockedTasks.length}</p>
<small>of ${tasks.total ?? 0} total · click to expand</small>
</div>
<a class="card card-link" href="#recent-activity">
<h3>Events Today</h3>
<p class="big-num">${todayCount}</p>
<small>last 20 shown below</small>
</a>
</div>
<div class="card ${(decisions.open + decisions.escalated) > 0 ? 'warn' : ''}">
<h3>Blocking Decisions</h3>
<p class="big-num">${(decisions.open ?? 0) + (decisions.escalated ?? 0)}</p>
<small>${decisions.escalated ?? 0} escalated</small>
<div id="blocked-panel" style="display:none;margin-bottom:1rem">
${blockedTasks.length === 0
? html`<p class="dim" style="padding:0.5rem 0">No tasks currently blocked.</p>`
: html`<div class="bt-list">${blockedTasks.map(t => {
const wsName = wsById[t.workstream_id]?.title ?? t.workstream_id?.slice(0,8) ?? "—";
return html`<div class="bt-row">
<div class="bt-meta">${wsName}</div>
<div class="bt-title">${t.title}</div>
${t.blocking_reason ? html`<div class="bt-reason">⊘ ${t.blocking_reason}</div>` : ""}
</div>`;
})}</div>`
}
</div>
<div class="card ${(tasks.blocked ?? 0) > 0 ? 'warn' : ''}">
<h3>Blocked Tasks</h3>
<p class="big-num">${tasks.blocked ?? 0}</p>
<small>of ${tasks.total ?? 0} total</small>
</div>
<div class="card">
<h3>Events Today</h3>
<p class="big-num">${(summary.recent_progress ?? []).filter(e =>
e.created_at?.startsWith(new Date().toISOString().slice(0,10))).length}</p>
<small>last 20 shown below</small>
</div>
</div>`);
</div>`;
statusEl.querySelector('[data-toggle="blocked-panel"]').addEventListener('click', () => {
const panel = statusEl.querySelector('#blocked-panel');
const isOpen = panel.style.display !== 'none';
panel.style.display = isOpen ? 'none' : 'block';
statusEl.querySelector('[data-toggle="blocked-panel"] small').textContent =
isOpen ? `of ${tasks.total ?? 0} total · click to expand` : `of ${tasks.total ?? 0} total · click to collapse`;
});
display(statusEl);
```
## What's next?
@@ -367,6 +398,13 @@ display(Inputs.table((summary.recent_progress ?? []).map(e => ({
.live-bar { font-size: 0.8rem; color: gray; text-align: right; margin-bottom: 0.5rem; }
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
.card.warn { border: 2px solid orange; }
.card-link { cursor: pointer; transition: box-shadow 0.15s, transform 0.1s; text-decoration: none; color: inherit; display: block; }
.card-link:hover { box-shadow: 0 3px 10px rgba(0,0,0,0.13); transform: translateY(-1px); }
.bt-list { display: flex; flex-direction: column; gap: 0.5rem; }
.bt-row { background: var(--theme-background-alt); border-radius: 6px; padding: 0.6rem 0.9rem; border-left: 3px solid #ff7043; }
.bt-meta { font-size: 0.7rem; color: gray; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.15rem; }
.bt-title { font-weight: 600; font-size: 0.9rem; }
.bt-reason { font-size: 0.8rem; color: #b45309; margin-top: 0.25rem; }
.big-num { font-size: 2.5rem; font-weight: bold; margin: 0.25rem 0; }
.warning { background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 0.75rem; }
.hint-box { background: var(--theme-background-alt); border-left: 3px solid steelblue; border-radius: 4px; padding: 0.75rem 1rem; margin-top: 0.75rem; font-size: 0.9rem; }