Files
the-custodian/state-hub/dashboard/src/progress.md
tegwick 05cc29e50b 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>
2026-02-24 17:47:49 +01:00

1.9 KiB

title
title
Progress

Progress Log

Append-only per constitution §5 — no deletions.

const events = await FileAttachment("data/progress.json").json();
const data = Array.isArray(events) ? events : [];
const authorFilter = view(Inputs.select(
  ["(all)", ...new Set(data.map(e => e.author ?? "unknown"))],
  { label: "Author" }
));
const typeFilter = view(Inputs.select(
  ["(all)", ...new Set(data.map(e => e.event_type))],
  { label: "Event type" }
));
const sinceFilter = view(Inputs.date({ label: "Since" }));
const filtered = data.filter(e =>
  (authorFilter === "(all)" || (e.author ?? "unknown") === authorFilter) &&
  (typeFilter === "(all)" || e.event_type === typeFilter) &&
  (!sinceFilter || new Date(e.created_at) >= sinceFilter)
);

display(html`<p><strong>${filtered.length}</strong> events shown (append-only, no deletions).</p>`);

display(Inputs.table(filtered.map(e => ({
  Time: new Date(e.created_at).toLocaleString(),
  Type: e.event_type,
  Author: e.author ?? "—",
  Summary: e.summary,
})), { rows: 50 }));

Event Volume (Last 30 Days)

import * as Plot from "npm:@observablehq/plot";

const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 30);

const byDay = data
  .filter(e => new Date(e.created_at) >= cutoff)
  .reduce((acc, e) => {
    const day = e.created_at.slice(0, 10);
    acc[day] = (acc[day] ?? 0) + 1;
    return acc;
  }, {});

display(Plot.plot({
  title: "Progress Events per Day (30-day window)",
  x: { label: "Date", tickRotate: -30 },
  y: { label: "Events", grid: true },
  marks: [
    Plot.areaY(
      Object.entries(byDay).sort().map(([day, count]) => ({ day, count })),
      { x: "day", y: "count", fill: "steelblue", fillOpacity: 0.3 }
    ),
    Plot.lineY(
      Object.entries(byDay).sort().map(([day, count]) => ({ day, count })),
      { x: "day", y: "count", stroke: "steelblue" }
    ),
    Plot.ruleY([0]),
  ],
  marginBottom: 60,
  width: 750,
}));