generated from coulomb/repo-seed
Live dashboard: replace data loaders with client-side polling
CORS: add CORSMiddleware to FastAPI for localhost:3000 so browser fetch works across ports without errors. All four pages now use async generator cells that call the API directly and re-yield every 15 s — no data loader cache, no manual cache clearing. Each page shows a live status bar (● green/red · last updated time). Offline state shows the `make api` hint inline. index.md: add "Registered Projects" section — polls /progress/?event_type=milestone&limit=500 and filters for "Project registered with State Hub:" events; shows project name, domain, path, and registration timestamp. workstreams.md: fix broken domain column — now fetches /workstreams/ and /topics/ in parallel and joins on topic_id client-side. Previously the domain column showed "unknown" for all rows because WorkstreamRead schema doesn't include domain. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,42 +2,69 @@
|
||||
title: Progress
|
||||
---
|
||||
|
||||
```js
|
||||
const API = "http://127.0.0.1:8000";
|
||||
const POLL = 15_000;
|
||||
```
|
||||
|
||||
```js
|
||||
const progState = (async function*() {
|
||||
while (true) {
|
||||
let data = [], ok = false;
|
||||
try {
|
||||
const r = await fetch(`${API}/progress/?limit=500`);
|
||||
ok = r.ok;
|
||||
data = ok ? await r.json() : [];
|
||||
} catch {}
|
||||
yield {data, ok, ts: new Date()};
|
||||
await new Promise(res => setTimeout(res, POLL));
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
```js
|
||||
const data = progState.data ?? [];
|
||||
const _ok = progState.ok ?? false;
|
||||
const _ts = progState.ts;
|
||||
```
|
||||
|
||||
# Progress Log
|
||||
|
||||
*Append-only per constitution §5 — no deletions.*
|
||||
|
||||
```js
|
||||
const events = await FileAttachment("data/progress.json").json();
|
||||
const data = Array.isArray(events) ? events : [];
|
||||
display(html`<div class="live-bar">
|
||||
<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>`);
|
||||
```
|
||||
|
||||
```js
|
||||
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 authorOpts = ["(all)", ...new Set(data.map(e => e.author ?? "unknown"))].sort();
|
||||
const typeOpts = ["(all)", ...new Set(data.map(e => e.event_type))].sort();
|
||||
|
||||
const authorFilter = view(Inputs.select(authorOpts, {label: "Author"}));
|
||||
const typeFilter = view(Inputs.select(typeOpts, {label: "Event type"}));
|
||||
const sinceFilter = view(Inputs.date({label: "Since"}));
|
||||
```
|
||||
|
||||
```js
|
||||
const filtered = data.filter(e =>
|
||||
(authorFilter === "(all)" || (e.author ?? "unknown") === authorFilter) &&
|
||||
(typeFilter === "(all)" || e.event_type === typeFilter) &&
|
||||
(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 ?? "—",
|
||||
Time: new Date(e.created_at).toLocaleString(),
|
||||
Type: e.event_type,
|
||||
Author: e.author ?? "—",
|
||||
Summary: e.summary,
|
||||
})), { rows: 50 }));
|
||||
})), {rows: 50}));
|
||||
```
|
||||
|
||||
## Event Volume (Last 30 Days)
|
||||
@@ -45,33 +72,34 @@ display(Inputs.table(filtered.map(e => ({
|
||||
```js
|
||||
import * as Plot from "npm:@observablehq/plot";
|
||||
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - 30);
|
||||
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const byDay = Object.entries(
|
||||
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;
|
||||
}, {})
|
||||
).sort().map(([day, count]) => ({day, count}));
|
||||
|
||||
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,
|
||||
}));
|
||||
display(byDay.length === 0
|
||||
? html`<p style="color:gray">No events in the last 30 days.</p>`
|
||||
: Plot.plot({
|
||||
title: "Progress events per day (30-day window)",
|
||||
x: {label: "Date", tickRotate: -30},
|
||||
y: {label: "Events", grid: true},
|
||||
marks: [
|
||||
Plot.areaY(byDay, {x: "day", y: "count", fill: "steelblue", fillOpacity: 0.3}),
|
||||
Plot.lineY(byDay, {x: "day", y: "count", stroke: "steelblue"}),
|
||||
Plot.ruleY([0]),
|
||||
],
|
||||
marginBottom: 60,
|
||||
width: 750,
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
<style>
|
||||
.live-bar { font-size: 0.8rem; color: gray; text-align: right; margin-bottom: 0.5rem; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user