123 lines
3.5 KiB
Markdown
123 lines
3.5 KiB
Markdown
|
|
---
|
||
|
|
title: Overview
|
||
|
|
---
|
||
|
|
|
||
|
|
# Custodian State Hub
|
||
|
|
|
||
|
|
```js
|
||
|
|
const summary = await FileAttachment("data/summary.json").json();
|
||
|
|
const totals = summary.totals ?? {};
|
||
|
|
const ws = totals.workstreams ?? {};
|
||
|
|
const tasks = totals.tasks ?? {};
|
||
|
|
const decisions = totals.decisions ?? {};
|
||
|
|
const topics = totals.topics ?? {};
|
||
|
|
```
|
||
|
|
|
||
|
|
```js
|
||
|
|
if (summary.error) display(html`<div class="warning">⚠️ API unreachable: ${summary.error}. Run <code>make api</code>.</div>`);
|
||
|
|
```
|
||
|
|
|
||
|
|
## 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-number">${ws.active ?? 0}</p>
|
||
|
|
<small>${ws.blocked ?? 0} blocked</small>
|
||
|
|
</div>
|
||
|
|
<div class="card ${(decisions.open + decisions.escalated) > 0 ? 'warn' : ''}">
|
||
|
|
<h3>Blocking Decisions</h3>
|
||
|
|
<p class="big-number">${(decisions.open ?? 0) + (decisions.escalated ?? 0)}</p>
|
||
|
|
<small>${decisions.escalated ?? 0} escalated</small>
|
||
|
|
</div>
|
||
|
|
<div class="card ${(tasks.blocked ?? 0) > 0 ? 'warn' : ''}">
|
||
|
|
<h3>Blocked Tasks</h3>
|
||
|
|
<p class="big-number">${tasks.blocked ?? 0}</p>
|
||
|
|
<small>of ${tasks.total ?? 0} total</small>
|
||
|
|
</div>
|
||
|
|
<div class="card">
|
||
|
|
<h3>Progress Events Today</h3>
|
||
|
|
<p class="big-number">${(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>`);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Tasks by Domain
|
||
|
|
|
||
|
|
```js
|
||
|
|
import * as Plot from "npm:@observablehq/plot";
|
||
|
|
|
||
|
|
const tasksByDomain = [];
|
||
|
|
for (const topic of (summary.topics ?? [])) {
|
||
|
|
for (const ws of (topic.workstreams ?? [])) {
|
||
|
|
// workstream stubs don't include tasks in summary — show per-topic WS count as proxy
|
||
|
|
}
|
||
|
|
tasksByDomain.push({ domain: topic.domain, status: topic.status, count: (topic.workstreams ?? []).length });
|
||
|
|
}
|
||
|
|
|
||
|
|
display(Plot.plot({
|
||
|
|
title: "Open Workstreams by Domain",
|
||
|
|
x: { label: "Domain" },
|
||
|
|
y: { label: "Count", grid: true },
|
||
|
|
marks: [
|
||
|
|
Plot.barY(tasksByDomain, { x: "domain", y: "count", fill: "domain", tip: true }),
|
||
|
|
Plot.ruleY([0]),
|
||
|
|
],
|
||
|
|
marginBottom: 80,
|
||
|
|
width: 700,
|
||
|
|
}));
|
||
|
|
```
|
||
|
|
|
||
|
|
## Blocking Decisions
|
||
|
|
|
||
|
|
```js
|
||
|
|
const blocking = summary.blocking_decisions ?? [];
|
||
|
|
if (blocking.length === 0) {
|
||
|
|
display(html`<p style="color:green">✓ No blocking decisions.</p>`);
|
||
|
|
} else {
|
||
|
|
display(Inputs.table(blocking.map(d => ({
|
||
|
|
Title: d.title,
|
||
|
|
Status: d.status,
|
||
|
|
Deadline: d.deadline ? new Date(d.deadline).toLocaleDateString() : "—",
|
||
|
|
Escalated: d.escalation_note ? "⚠️" : "",
|
||
|
|
}))));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Decisions Due Within 7 Days
|
||
|
|
|
||
|
|
```js
|
||
|
|
const now = new Date();
|
||
|
|
const in7 = new Date(now.getTime() + 7*24*60*60*1000);
|
||
|
|
const due = (summary.blocking_decisions ?? []).filter(d => d.deadline && new Date(d.deadline) <= in7);
|
||
|
|
if (due.length === 0) {
|
||
|
|
display(html`<p>No decisions due in next 7 days.</p>`);
|
||
|
|
} else {
|
||
|
|
display(Inputs.table(due.map(d => ({
|
||
|
|
Title: d.title,
|
||
|
|
Deadline: new Date(d.deadline).toLocaleString(),
|
||
|
|
Status: d.status,
|
||
|
|
}))));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Recent Activity
|
||
|
|
|
||
|
|
```js
|
||
|
|
display(Inputs.table((summary.recent_progress ?? []).map(e => ({
|
||
|
|
Time: new Date(e.created_at).toLocaleString(),
|
||
|
|
Type: e.event_type,
|
||
|
|
Author: e.author ?? "—",
|
||
|
|
Summary: e.summary,
|
||
|
|
})), { maxWidth: 900 }));
|
||
|
|
```
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
|
||
|
|
.card.warn { border: 2px solid orange; }
|
||
|
|
.big-number { font-size: 2.5rem; font-weight: bold; margin: 0.25rem 0; }
|
||
|
|
.warning { background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 0.75rem; }
|
||
|
|
</style>
|