---
title: Progress
---

```js
import {POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
```

```js
const progState = (async function*() {
  let failures = 0;
  while (true) {
    let data = [], tokenEvents = [], ok = false;
    try {
      const [r1, r2] = await Promise.all([
        apiFetch("/progress/?limit=500"),
        apiFetch("/token-events/?limit=1000"),
      ]);
      ok = r1.ok;
      data = ok ? await r1.json() : [];
      tokenEvents = r2.ok ? await r2.json() : [];
    } catch {}
    failures = ok ? 0 : failures + 1;
    yield {data, tokenEvents, ok, ts: new Date()};
    await waitForVisible(pollDelay({ok, base: POLL_HEAVY, failures}));
  }
})();
```

```js
const data        = progState.data        ?? [];
const tokenEvents = progState.tokenEvents ?? [];
const _ok  = progState.ok  ?? false;
const _ts  = progState.ts;
```

# Progress Log

```js
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`
    : html`<span style="color:red">Offline — run: <code>make api</code></span>`}
</div>`;
withDocHelp(_liveEl, "/docs/live-data");
injectTocTop("live-indicator", _liveEl);

const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/progress-log"); }
```

## Event Volume (Last 30 Days)

```js
import * as Plot from "npm:@observablehq/plot";

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 d = e.created_at.slice(0, 10); acc[d] = (acc[d] ?? 0) + 1; return acc; }, {})
).sort().map(([day, count]) => ({day, count}));

const tokensByDay = Object.entries(
  tokenEvents
    .filter(e => new Date(e.created_at) >= cutoff)
    .reduce((acc, e) => { const d = e.created_at.slice(0, 10); acc[d] = (acc[d] ?? 0) + (e.tokens_in || 0) + (e.tokens_out || 0); return acc; }, {})
).sort().map(([day, tokens]) => ({day, tokens}));

// Scale tokens onto the events axis for dual-axis display
const maxEvents = Math.max(1, ...byDay.map(d => d.count));
const maxTokens = Math.max(1, ...tokensByDay.map(d => d.tokens));
const ratio = maxTokens / maxEvents;
const fmtTokens = v => { const t = v * ratio; return t >= 1e6 ? (t/1e6).toFixed(1)+"M" : t >= 1e3 ? Math.round(t/1e3)+"k" : String(Math.round(t)); };

display(byDay.length === 0
  ? html`<p style="color:gray">No events in the last 30 days.</p>`
  : Plot.plot({
      title: "Progress events & tokens per day (30-day window)",
      x: {label: "Date", tickRotate: -30},
      y: {label: "← Events", grid: true},
      marginBottom: 60,
      marginRight: tokensByDay.length > 0 ? 70 : 20,
      width: 750,
      marks: [
        Plot.areaY(byDay, {x: "day", y: "count", fill: "steelblue", fillOpacity: 0.25}),
        Plot.lineY(byDay, {x: "day", y: "count", stroke: "steelblue", strokeWidth: 1.5}),
        Plot.tip(byDay, Plot.pointerX({x: "day", y: "count",
          title: d => `${d.day}\n${d.count} event${d.count === 1 ? "" : "s"}`})),
        ...(tokensByDay.length > 0 ? [
          Plot.areaY(tokensByDay, {x: "day", y: d => d.tokens / ratio, fill: "#f28e2b", fillOpacity: 0.2}),
          Plot.lineY(tokensByDay, {x: "day", y: d => d.tokens / ratio, stroke: "#f28e2b", strokeWidth: 1.5}),
          Plot.tip(tokensByDay, Plot.pointerX({x: "day", y: d => d.tokens / ratio,
            title: d => `${d.day}\n${d.tokens.toLocaleString()} tokens`})),
          Plot.axisY({anchor: "right", label: "Tokens →", tickFormat: fmtTokens}),
        ] : []),
        Plot.ruleY([0]),
      ],
    })
);

if (byDay.length > 0) display(html`<div style="font-size:0.78rem;display:flex;gap:1.2rem;margin-top:0.4rem;color:var(--theme-foreground-muted)">
  <span><span style="color:steelblue">▬</span> Events (left axis)</span>
  ${tokensByDay.length > 0 ? html`<span><span style="color:#f28e2b">▬</span> Tokens (right axis)</span>` : html`<span style="font-style:italic">No token data yet</span>`}
</div>`);
```

## Event Log

```js
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) &&
  (!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}));
```

<style>
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
</style>
