generated from coulomb/repo-seed
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure: PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable Framework telemetry dashboard. - state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard) - 5 DB tables: topics, workstreams, tasks, decisions, progress_events - 11 MCP tools + 5 resources registered in .mcp.json - Observable dashboard: Overview, Workstreams, Decisions, Progress pages - CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual) - ~/.claude/CLAUDE.md: global cross-project reference to the hub - scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
70
dashboard/src/decisions.md
Normal file
70
dashboard/src/decisions.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Decisions
|
||||
---
|
||||
|
||||
# Decisions
|
||||
|
||||
```js
|
||||
const decisions = await FileAttachment("data/decisions.json").json();
|
||||
const data = Array.isArray(decisions) ? decisions : [];
|
||||
const pending = data.filter(d => d.decision_type === "pending");
|
||||
const made = data.filter(d => d.decision_type === "made");
|
||||
```
|
||||
|
||||
```js
|
||||
const tab = view(Inputs.select(["Pending", "Made"], { label: "View" }));
|
||||
```
|
||||
|
||||
```js
|
||||
const shown = tab === "Pending" ? pending : made;
|
||||
|
||||
display(Inputs.table(shown.map(d => ({
|
||||
Title: d.title,
|
||||
Status: d.status + (d.escalation_note ? " ⚠️" : ""),
|
||||
Decided_by: d.decided_by ?? "—",
|
||||
Deadline: d.deadline ? new Date(d.deadline).toLocaleDateString() : "—",
|
||||
Rationale: (d.rationale ?? "").slice(0, 80),
|
||||
Updated: new Date(d.updated_at).toLocaleDateString(),
|
||||
})), { rows: 30 }));
|
||||
```
|
||||
|
||||
## Resolution Velocity
|
||||
|
||||
```js
|
||||
import * as Plot from "npm:@observablehq/plot";
|
||||
|
||||
const resolved = made.filter(d => d.decided_at);
|
||||
const byMonth = resolved.reduce((acc, d) => {
|
||||
const m = d.decided_at.slice(0, 7);
|
||||
acc[m] = (acc[m] ?? 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
display(Plot.plot({
|
||||
title: "Decisions Resolved per Month",
|
||||
x: { label: "Month", tickRotate: -30 },
|
||||
y: { label: "Count", grid: true },
|
||||
marks: [
|
||||
Plot.barY(
|
||||
Object.entries(byMonth).map(([month, count]) => ({ month, count })),
|
||||
{ x: "month", y: "count", fill: "steelblue", tip: true }
|
||||
),
|
||||
Plot.ruleY([0]),
|
||||
],
|
||||
marginBottom: 60,
|
||||
width: 700,
|
||||
}));
|
||||
```
|
||||
|
||||
```js
|
||||
if (tab === "Pending" && pending.filter(d => d.escalation_note).length > 0) {
|
||||
display(html`<div class="escalation-box">
|
||||
<strong>⚠️ Escalated decisions require human approval before any action is taken (constitution §4).</strong>
|
||||
<ul>${pending.filter(d => d.escalation_note).map(d => html`<li><b>${d.title}</b>: ${d.escalation_note}</li>`)}</ul>
|
||||
</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
<style>
|
||||
.escalation-box { background: #fff3cd; border: 2px solid orange; border-radius: 8px; padding: 1rem; margin-top: 1rem; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user