78 lines
1.9 KiB
Markdown
78 lines
1.9 KiB
Markdown
|
|
---
|
||
|
|
title: Progress
|
||
|
|
---
|
||
|
|
|
||
|
|
# Progress Log
|
||
|
|
|
||
|
|
*Append-only per constitution §5 — no deletions.*
|
||
|
|
|
||
|
|
```js
|
||
|
|
const events = await FileAttachment("data/progress.json").json();
|
||
|
|
const data = Array.isArray(events) ? events : [];
|
||
|
|
```
|
||
|
|
|
||
|
|
```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" }));
|
||
|
|
```
|
||
|
|
|
||
|
|
```js
|
||
|
|
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)
|
||
|
|
|
||
|
|
```js
|
||
|
|
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,
|
||
|
|
}));
|
||
|
|
```
|