diff --git a/dashboard/observablehq.config.js b/dashboard/observablehq.config.js index d7fc3b2..9ffe7dd 100644 --- a/dashboard/observablehq.config.js +++ b/dashboard/observablehq.config.js @@ -72,6 +72,7 @@ export default { pages: [ { name: "Capabilities", path: "/docs/capabilities" }, { name: "Connecting to the Hub", path: "/docs/connecting" }, + { name: "Dashboard", path: "/docs/dashboard" }, { name: "Contributions", path: "/docs/contributions" }, { name: "Decision Health", path: "/docs/decisions-kpi" }, { name: "Decisions", path: "/docs/decisions" }, diff --git a/dashboard/src/docs/dashboard.md b/dashboard/src/docs/dashboard.md new file mode 100644 index 0000000..7b07b08 --- /dev/null +++ b/dashboard/src/docs/dashboard.md @@ -0,0 +1,338 @@ +--- +title: Dashboard — Technical Reference +--- + +# State Hub Dashboard — Technical Reference + +The State Hub dashboard is the primary visual interface for the Custodian +ecosystem. It provides live, reactive views of all tracked domains, +workstreams, tasks, decisions, contributions, SBOM data, and agent activity — +all sourced from the local FastAPI state service. + +--- + +## Framework: Observable Framework + +The dashboard is built on **[Observable Framework](https://observablehq.com/framework/)**, +an open-source static-site framework from Observable, Inc. designed specifically +for data-driven pages. + +### Why Observable Framework? + +| Requirement | How Observable Framework satisfies it | +|---|---| +| **Local-first, no build-time cloud dependency** | Compiles to a static site (`npm run build`); the preview server and data loaders run entirely on localhost. | +| **Live data without a separate frontend service** | Pages poll the FastAPI backend directly from the browser via `fetch`. No BFF, no GraphQL, no WebSockets required. | +| **Reactive updates without React complexity** | Observable's cell-based execution model re-runs any code block whose inputs change. Async generators produce new values every poll cycle and trigger re-renders automatically. | +| **No JS bundler configuration** | `.md` files containing fenced JS code blocks are the entire source. No webpack, no Vite config, no `tsconfig.json`. | +| **Native data visualisation** | First-class integration with `@observablehq/plot` — a concise, grammar-of-graphics library — for all charts. | +| **Sovereignty-compatible** | The built output is a folder of static HTML/JS/CSS. It can be served by any web server, archived, or opened directly from disk. | +| **Offline-graceful** | Data loaders (Python scripts that run at build time) produce JSON snapshots. If the API is unreachable at build time, the loader emits an empty-structure JSON so the page still renders with a clear error state instead of crashing. | + +Observable Framework was chosen over alternatives (Grafana, Metabase, Streamlit, +Next.js) because its design principles are uniquely aligned with the Custodian +philosophy: **local-first**, **no vendor lock-in**, **sovereignty-preserving**, +and **auditable** — the full data pipeline is visible in plain Markdown files. + +--- + +## Architecture + +``` +src/ + observablehq.config.js — site metadata, page registry, theme, global head + components/ — shared JS modules + data/ — Python data loaders (run at build time) + docs/ — reference pages (this file lives here) + *.md — one page per feature area +``` + +### Data flow + +There are two complementary data-fetching strategies: + +**1. Static data loaders** (`src/data/*.json.py`) + +Python scripts executed by the Observable build toolchain at `npm run build` +or `npm run dev`. Each script calls the FastAPI backend via `urllib`, serialises +the response to JSON on stdout, and Observable Framework captures that output +as a static snapshot file that the page imports with `FileAttachment(...)`. + +Current loaders: + +| File | API endpoint | +|---|---| +| `summary.json.py` | `/state/summary` | +| `workstreams.json.py` | `/workstreams/` | +| `contributions.json.py` | `/contributions/` | +| `decisions.json.py` | `/decisions/` | +| `domains.json.py` | `/domains/` | +| `messages.json.py` | `/messages/` | +| `progress.json.py` | `/progress/` | +| `repos.json.py` | `/repos/` | +| `sbom.json.py` | `/sbom/aggregated` | +| `gitea-inventory.json.py` | Gitea instance inventory | + +**2. Live browser polling** (async generators in page `.md` files) + +All interactive pages bypass the static snapshots for live data by using +Observable's async generator pattern directly in the browser: + +```js +const summaryState = (async function*() { + while (true) { + const r = await fetch(`${API}/state/summary`); + yield { data: r.ok ? await r.json() : {error: `HTTP ${r.status}`}, ok: r.ok }; + await new Promise(res => setTimeout(res, POLL)); + } +})(); +``` + +`POLL` is set to **15 000 ms** (15 seconds) in `src/components/config.js`. +Observable's reactivity engine detects each new yield value and re-runs all +dependent code blocks, updating charts, tables, and KPI cards automatically. +A `●` live indicator in the top-left corner of each page shows the connection +status and the last-updated time. + +### Global configuration — `observablehq.config.js` + +| Setting | Value | +|---|---| +| Root directory | `src/` | +| Site title | "Custodian State Hub" | +| Theme | `["air", "near-midnight"]` — light body with dark sidebar | +| Favicon | Inline SVG data URI (🗄️ emoji) | +| Global head | KPI infobox styles, filter-bar styles, improvement-modal script | + +The `improvement-modal.js` component is injected at the config level rather +than imported per-page because Observable proxies `src/*.js` through its own +bundler, which prevents them from being loaded as raw `