diff --git a/dashboard/src/decisions.md b/dashboard/src/decisions.md
index 7b98581..4bb54a4 100644
--- a/dashboard/src/decisions.md
+++ b/dashboard/src/decisions.md
@@ -151,16 +151,23 @@ const _kpiBox = html`
withDocHelp(_kpiBox, "/docs/decisions-kpi");
-// ── Build sidebar widget: live indicator + KPI box ──────────────────────────
+// ── Build live indicator ────────────────────────────────────────────────────
const _liveEl = html`
●
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()}`
: html`Offline — run: make api`}
`;
+withDocHelp(_liveEl, "/docs/live-data");
-const _tocInjected = injectTocTop("decisions-sidebar", html`
${_liveEl}${_kpiBox}
`);
-if (!_tocInjected) display(html`
${_liveEl}${_kpiBox}
`);
+// ── Inject into TOC sidebar: KPI first (prepend → bottom), live last (→ top) ─
+const _toc = document.querySelector("#observablehq-toc");
+if (_toc) {
+ injectTocTop("decisions-kpi-box", _kpiBox);
+ injectTocTop("live-indicator", _liveEl);
+} else {
+ display(html`
${_liveEl}${_kpiBox}
`);
+}
```
## Resolution History
@@ -357,8 +364,9 @@ if (escalated.length > 0) {
.live-indicator {
font-size: 0.8rem;
color: gray;
- text-align: right;
margin-bottom: 0.75rem;
+ position: relative;
+ padding-right: 1.6rem;
}
/* ── KPI infobox ──────────────────────────────────────────────────────────── */
diff --git a/dashboard/src/docs/live-data.md b/dashboard/src/docs/live-data.md
new file mode 100644
index 0000000..53edf69
--- /dev/null
+++ b/dashboard/src/docs/live-data.md
@@ -0,0 +1,62 @@
+---
+title: Live Data — Reference
+---
+
+# Live Data — How the Dashboard Refreshes
+
+All dashboard pages poll the State Hub API automatically. No manual refresh is ever needed.
+
+---
+
+## Poll interval
+
+Every page fetches fresh data from `http://127.0.0.1:8000` every **15 seconds** using an async generator loop. The previous data stays visible while the next request is in flight, so the UI never goes blank.
+
+---
+
+## The live indicator
+
+The **●** dot in the top-right corner of each page shows the current connection state:
+
+| Indicator | Meaning |
+|---|---|
+| **● Live · updated HH:MM:SS** | Last poll succeeded — data is current as of that time |
+| **● Offline — run: `make api`** | API is unreachable — the dot turns red |
+
+The timestamp updates on every successful poll. If you see a time that is more than ~30 seconds in the past, the poll is stalled (browser tab backgrounded or network issue) — reloading the page resets the loop.
+
+---
+
+## Offline recovery
+
+If the API goes offline while you are viewing a page:
+
+1. The indicator turns red immediately on the next failed poll (within 15 s)
+2. The last successfully loaded data stays visible
+3. Once the API restarts, the indicator turns green on the next poll — **no page reload needed**
+
+To restart the API:
+
+```bash
+cd ~/the-custodian/state-hub
+make api # starts uvicorn on 127.0.0.1:8000
+# or, if postgres is not running:
+make start # db + migrate + api
+```
+
+---
+
+## Which data each page polls
+
+| Page | Endpoints |
+|---|---|
+| Overview | `/state/summary` |
+| Workstreams | `/workstreams/`, `/topics/`, `/state/summary` |
+| Decisions | `/decisions/?limit=500`, `/topics/` |
+| Progress | `/progress/?limit=500` |
+
+All endpoints are read-only GET requests. The dashboard never writes to the API.
+
+---
+
+*Poll interval: 15 s. Data is refreshed in the background — the page never reloads itself.*
diff --git a/dashboard/src/index.md b/dashboard/src/index.md
index f53b2cc..10c9fa9 100644
--- a/dashboard/src/index.md
+++ b/dashboard/src/index.md
@@ -68,12 +68,17 @@ const regsState = (async function*() {
# Custodian State Hub
```js
-display(html`
+import {injectTocTop} from "./components/toc-sidebar.js";
+import {withDocHelp} from "./components/doc-overlay.js";
+
+const _liveEl = html`
●
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()}`
- : `Offline — run: cd ~/the-custodian/state-hub && make api`}
-
`);
+ : html`
Offline — run: cd ~/the-custodian/state-hub && make api`}
+
`;
+withDocHelp(_liveEl, "/docs/live-data");
+injectTocTop("live-indicator", _liveEl);
```
```js
@@ -395,7 +400,7 @@ display(Inputs.table((summary.recent_progress ?? []).map(e => ({
```
diff --git a/dashboard/src/workstreams.md b/dashboard/src/workstreams.md
index 5939c3b..7031b9a 100644
--- a/dashboard/src/workstreams.md
+++ b/dashboard/src/workstreams.md
@@ -47,12 +47,17 @@ const _ts = wsState.ts;
# Workstreams
```js
-display(html`
+import {injectTocTop} from "./components/toc-sidebar.js";
+import {withDocHelp} from "./components/doc-overlay.js";
+
+const _liveEl = html`
●
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()}`
- : `Offline — run: make api`}
-
`);
+ : html`
Offline — run: make api`}
+
`;
+withDocHelp(_liveEl, "/docs/live-data");
+injectTocTop("live-indicator", _liveEl);
```
```js
@@ -145,7 +150,7 @@ if (wsWithDeps.length === 0) {
```