dashboard: move live indicator to TOC sidebar on all pages; add live-data docs
- All four pages (index, workstreams, decisions, progress) now inject the
live indicator into #observablehq-toc via injectTocTop("live-indicator", el)
Left-aligned (no text-align: right), position:relative + padding-right for
the ? button affordance
- decisions.md: splits the former combined "decisions-sidebar" widget into two
separate injectTocTop calls — KPI box first (ends lower), live indicator
second (ends at top); both now have their own stable ids
- withDocHelp(_liveEl, "/docs/live-data") wires the ? button on every page
- src/docs/live-data.md: new documentation page explaining poll interval (15s),
indicator colour semantics, offline recovery, and which endpoints each page hits
- Removes the .live-bar CSS class from all pages; replaces with .live-indicator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -151,16 +151,23 @@ const _kpiBox = html`<div class="kpi-infobox">
|
||||
|
||||
withDocHelp(_kpiBox, "/docs/decisions-kpi");
|
||||
|
||||
// ── Build sidebar widget: live indicator + KPI box ──────────────────────────
|
||||
// ── Build live indicator ────────────────────────────────────────────────────
|
||||
const _liveEl = html`<div class="live-indicator">
|
||||
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
||||
${_ok
|
||||
? `Live · updated ${_ts?.toLocaleTimeString()}`
|
||||
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
|
||||
</div>`;
|
||||
withDocHelp(_liveEl, "/docs/live-data");
|
||||
|
||||
const _tocInjected = injectTocTop("decisions-sidebar", html`<div>${_liveEl}${_kpiBox}</div>`);
|
||||
if (!_tocInjected) display(html`<div style="float:right;width:215px;margin-left:1rem">${_liveEl}${_kpiBox}</div>`);
|
||||
// ── 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`<div style="float:right;width:215px;margin-left:1rem">${_liveEl}${_kpiBox}</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
## 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 ──────────────────────────────────────────────────────────── */
|
||||
|
||||
62
state-hub/dashboard/src/docs/live-data.md
Normal file
62
state-hub/dashboard/src/docs/live-data.md
Normal file
@@ -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.*
|
||||
@@ -68,12 +68,17 @@ const regsState = (async function*() {
|
||||
# Custodian State Hub
|
||||
|
||||
```js
|
||||
display(html`<div class="live-bar">
|
||||
import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
|
||||
const _liveEl = html`<div class="live-indicator">
|
||||
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
||||
${_ok
|
||||
? `Live · updated ${_ts?.toLocaleTimeString()}`
|
||||
: `<span style="color:red">Offline — run: <code>cd ~/the-custodian/state-hub && make api</code></span>`}
|
||||
</div>`);
|
||||
: html`<span style="color:red">Offline — run: <code>cd ~/the-custodian/state-hub && make api</code></span>`}
|
||||
</div>`;
|
||||
withDocHelp(_liveEl, "/docs/live-data");
|
||||
injectTocTop("live-indicator", _liveEl);
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -395,7 +400,7 @@ display(Inputs.table((summary.recent_progress ?? []).map(e => ({
|
||||
```
|
||||
|
||||
<style>
|
||||
.live-bar { font-size: 0.8rem; color: gray; text-align: right; margin-bottom: 0.5rem; }
|
||||
.live-indicator { font-size: 0.8rem; color: gray; margin-bottom: 0.75rem; position: relative; padding-right: 1.6rem; }
|
||||
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
|
||||
.card.warn { border: 2px solid orange; }
|
||||
.card-link { cursor: pointer; transition: box-shadow 0.15s, transform 0.1s; text-decoration: none; color: inherit; display: block; }
|
||||
|
||||
@@ -33,12 +33,17 @@ const _ts = progState.ts;
|
||||
*Append-only per constitution §5 — no deletions.*
|
||||
|
||||
```js
|
||||
display(html`<div class="live-bar">
|
||||
import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
|
||||
const _liveEl = html`<div class="live-indicator">
|
||||
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
||||
${_ok
|
||||
? `Live · updated ${_ts?.toLocaleTimeString()} · ${data.length} events total`
|
||||
: `<span style="color:red">Offline — run: <code>make api</code></span>`}
|
||||
</div>`);
|
||||
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
|
||||
</div>`;
|
||||
withDocHelp(_liveEl, "/docs/live-data");
|
||||
injectTocTop("live-indicator", _liveEl);
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -101,5 +106,5 @@ display(byDay.length === 0
|
||||
```
|
||||
|
||||
<style>
|
||||
.live-bar { font-size: 0.8rem; color: gray; text-align: right; margin-bottom: 0.5rem; }
|
||||
.live-indicator { font-size: 0.8rem; color: gray; margin-bottom: 0.75rem; position: relative; padding-right: 1.6rem; }
|
||||
</style>
|
||||
|
||||
@@ -47,12 +47,17 @@ const _ts = wsState.ts;
|
||||
# Workstreams
|
||||
|
||||
```js
|
||||
display(html`<div class="live-bar">
|
||||
import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
|
||||
const _liveEl = html`<div class="live-indicator">
|
||||
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
||||
${_ok
|
||||
? `Live · updated ${_ts?.toLocaleTimeString()}`
|
||||
: `<span style="color:red">Offline — run: <code>make api</code></span>`}
|
||||
</div>`);
|
||||
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
|
||||
</div>`;
|
||||
withDocHelp(_liveEl, "/docs/live-data");
|
||||
injectTocTop("live-indicator", _liveEl);
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -145,7 +150,7 @@ if (wsWithDeps.length === 0) {
|
||||
```
|
||||
|
||||
<style>
|
||||
.live-bar { font-size: 0.8rem; color: gray; text-align: right; margin-bottom: 0.5rem; }
|
||||
.live-indicator { font-size: 0.8rem; color: gray; margin-bottom: 0.75rem; position: relative; padding-right: 1.6rem; }
|
||||
.dim { color: gray; font-style: italic; }
|
||||
.filter-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 1rem; }
|
||||
.filter-owner { display: flex; align-items: center; }
|
||||
|
||||
Reference in New Issue
Block a user