Files
the-custodian/state-hub/dashboard/src/token-cost.md
tegwick 41ce4ede2c feat(dashboard): poll optimisation — T4, T5, T6
T4: workstreams.md and dependencies.md now call /state/deps instead of the
    full /state/summary — removes 2 heavy 10-table queries per 60 s cycle.

T5: index.md's 4 independent polling loops (summaryState, sbomSnapState,
    regsState, wsChartState) consolidated into a single pageState generator
    with one Promise.all batch and a shared backoff counter.

T6: config.js gains waitForVisible(ms) — pauses polling entirely while the
    tab is hidden and fires immediately on visibilitychange.  pollDelay()
    simplified (hidden-tab POLL_HIDDEN logic removed).  All 16 polling pages
    migrated from await sleep(pollDelay(...)) to await waitForVisible(pollDelay(...)).

CUST-WP-0039 complete — all 6 tasks done.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 17:58:18 +02:00

10 KiB

title
title
Token Cost
import {apiFetch, pollDelay, waitForVisible} from "./components/config.js";
import {refCell} from "./components/ref-cell.js";
const POLL = 60_000;
// Fetch token events, by-repo summary, workstreams, and tasks in parallel
const tokenState = (async function*() {
  let failures = 0;
  while (true) {
    let byRepo = [], events = [], wsMap = {}, taskMap = {}, ok = false;
    try {
      const [r1, r2, r3, r4] = await Promise.all([
        apiFetch("/token-events/by-repo/"),
        apiFetch("/token-events/?limit=1000"),
        apiFetch("/workstreams/"),
        apiFetch("/tasks/"),
      ]);
      ok = r1.ok && r2.ok;
      if (ok) {
        byRepo = await r1.json();
        events = await r2.json();
      }
      if (r3.ok) {
        const wsList = await r3.json();
        for (const w of wsList) wsMap[w.id] = w;
      }
      if (r4.ok) {
        const taskList = await r4.json();
        for (const t of taskList) taskMap[t.id] = t;
      }
    } catch {}
    failures = ok ? 0 : failures + 1;
    yield {byRepo, events, wsMap, taskMap, ok, ts: new Date()};
    await waitForVisible(pollDelay({ok, base: POLL, failures}));
  }
})();
// Resolve an event's repo_id via the 3-level chain: direct → workstream → task→workstream
function resolveRepoId(e, wsMap, taskMap) {
  if (e.repo_id) return e.repo_id;
  const wsId = e.workstream_id ?? taskMap[e.task_id]?.workstream_id;
  return wsId ? (wsMap[wsId]?.repo_id ?? null) : null;
}

function buildSummary(events) {
  const byWs = {}, byModel = {}, byTask = {};
  for (const e of events) {
    const tot = (e.tokens_in || 0) + (e.tokens_out || 0);
    if (e.workstream_id) {
      byWs[e.workstream_id] = byWs[e.workstream_id] || {scope_id: e.workstream_id, tokens_in: 0, tokens_out: 0, event_count: 0};
      byWs[e.workstream_id].tokens_in  += e.tokens_in  || 0;
      byWs[e.workstream_id].tokens_out += e.tokens_out || 0;
      byWs[e.workstream_id].event_count++;
    }
    const model = e.model || "unknown";
    byModel[model] = (byModel[model] || 0) + tot;
    if (e.task_id) {
      byTask[e.task_id] = byTask[e.task_id] || {task_id: e.task_id, tokens_in: 0, tokens_out: 0, event_count: 0};
      byTask[e.task_id].tokens_in  += e.tokens_in  || 0;
      byTask[e.task_id].tokens_out += e.tokens_out || 0;
      byTask[e.task_id].event_count++;
    }
  }
  const toRows = obj => Object.values(obj)
    .map(v => ({...v, tokens_total: (v.tokens_in || 0) + (v.tokens_out || 0)}))
    .sort((a, b) => b.tokens_total - a.tokens_total);
  return {
    by_workstream: toRows(byWs),
    by_model: Object.entries(byModel)
      .map(([model, tokens_total]) => ({model, tokens_total}))
      .sort((a, b) => b.tokens_total - a.tokens_total),
    top_tasks: toRows(byTask),
    total_events: events.length,
  };
}

function nameCell(name, fullName) {
  const s = String(name ?? fullName ?? "—");
  const full = String(fullName ?? name ?? "—");
  const el = document.createElement("span");
  el.title = full;
  el.textContent = s.length > 80 ? s.slice(0, 80) + "…" : s;
  return el;
}

function sortRows(rows, sortField) {
  if (sortField === "Tokens Total") return rows; // already sorted by buildSummary / by-repo API
  const s = [...rows];
  if      (sortField === "Tokens In")   s.sort((a, b) => (b.tokens_in   || 0) - (a.tokens_in   || 0));
  else if (sortField === "Tokens Out")  s.sort((a, b) => (b.tokens_out  || 0) - (a.tokens_out  || 0));
  else if (sortField === "Event Count") s.sort((a, b) => (b.event_count || 0) - (a.event_count || 0));
  else if (sortField === "Most Recent") s.sort((a, b) => (b._lastAt     || 0) - (a._lastAt     || 0));
  return s;
}
const byRepo  = tokenState.byRepo  ?? [];
const events  = tokenState.events  ?? [];
const wsMap   = tokenState.wsMap   ?? {};
const taskMap = tokenState.taskMap ?? {};
const _ok = tokenState.ok ?? false;
const _ts = tokenState.ts;

Token Cost

display(html`<div style="font-size:0.8rem;color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">
${_ok ? `Live · ${_ts?.toLocaleTimeString()} · ${events.length} events` : "API offline"}
</div>`);
const repoSel = Inputs.select(
  ["All repos", ...byRepo.map(r => r.repo_slug)],
  {label: "Filter by repo"}
);
const sortSel = Inputs.select(
  ["Tokens Total", "Tokens In", "Tokens Out", "Event Count", "Most Recent"],
  {label: "Sort by"}
);
const maxSel = Inputs.select(
  [10, 20, 50, 100, 500],
  {value: 20, label: "Show"}
);
display(html`<div style="display:flex;gap:1.5rem;align-items:flex-end;flex-wrap:wrap;margin:0.5rem 0 1.5rem">${repoSel}${sortSel}${maxSel}</div>`);
const repoFilter = view(repoSel);
const sortOrder  = view(sortSel);
const maxResults = view(maxSel);
// Build filtered and last-event-annotated row sets
const selectedRepoId = repoFilter === "All repos"
  ? null
  : (byRepo.find(r => r.repo_slug === repoFilter)?.repo_id ?? null);

const filteredEvents = selectedRepoId
  ? events.filter(e => resolveRepoId(e, wsMap, taskMap) === selectedRepoId)
  : events;

const lastAtByRepo = {}, lastAtByWs = {}, lastAtByTask = {};
for (const e of filteredEvents) {
  const t = e.created_at ? new Date(e.created_at).getTime() : 0;
  const rid = resolveRepoId(e, wsMap, taskMap);
  if (rid) lastAtByRepo[rid] = Math.max(lastAtByRepo[rid] || 0, t);
  if (e.workstream_id) lastAtByWs[e.workstream_id] = Math.max(lastAtByWs[e.workstream_id] || 0, t);
  if (e.task_id) lastAtByTask[e.task_id] = Math.max(lastAtByTask[e.task_id] || 0, t);
}

const filteredByRepo = (selectedRepoId
  ? byRepo.filter(r => r.repo_id === selectedRepoId)
  : byRepo
).map(r => ({...r, _lastAt: lastAtByRepo[r.repo_id] || 0}));

const summary      = buildSummary(filteredEvents);
const wsRowsFull   = summary.by_workstream.map(r => ({...r, _lastAt: lastAtByWs[r.scope_id]  || 0}));
const taskRowsFull = summary.top_tasks.map(r    => ({...r, _lastAt: lastAtByTask[r.task_id]   || 0}));

By Repo

{
  const sorted = sortRows(filteredByRepo, sortOrder);
  const total  = sorted.length;
  const rows   = sorted.slice(0, maxResults);

  if (rows.length === 0) {
    display(html`<p style="color:var(--theme-foreground-muted)">No token events with repo association yet.</p>`);
  } else {
    display(Plot.plot({
      title: "Token consumption by repo",
      marginLeft: 160,
      width: Math.min(900, width),
      x: {label: "Tokens", tickFormat: "~s"},
      y: {label: null},
      color: {legend: true, domain: ["tokens_in", "tokens_out"], range: ["#4e79a7","#f28e2b"]},
      marks: [
        Plot.barX(
          rows.flatMap(r => [
            {repo: r.repo_slug, type: "tokens_in",  value: r.tokens_in},
            {repo: r.repo_slug, type: "tokens_out", value: r.tokens_out},
          ]),
          {x: "value", y: "repo", fill: "type", tip: true}
        ),
      ],
    }));

    display(Inputs.table(rows.map((r, i) => ({...r, _ref: i})), {
      columns: ["_ref", "repo_slug", "tokens_in", "tokens_out", "tokens_total", "event_count"],
      header: {
        _ref: "REF",
        repo_slug: "Repo",
        tokens_in: "Tokens In",
        tokens_out: "Tokens Out",
        tokens_total: "Total",
        event_count: "Events",
      },
      format: {
        _ref:         (_, i) => refCell(i + 1, "repos", rows[i].repo_slug),
        repo_slug:    d => nameCell(d, d),
        tokens_in:    d => d.toLocaleString(),
        tokens_out:   d => d.toLocaleString(),
        tokens_total: d => d.toLocaleString(),
      },
      width: {_ref: 50, repo_slug: 160, tokens_in: 110, tokens_out: 110, tokens_total: 110, event_count: 80},
    }));

    if (total > maxResults)
      display(html`<p style="font-size:0.8rem;color:var(--theme-foreground-muted);margin-top:0.25rem">Showing ${maxResults} of ${total} repos</p>`);
  }
}

By Workplan

{
  const sorted = sortRows(wsRowsFull, sortOrder);
  const total  = sorted.length;
  const rows   = sorted.slice(0, maxResults);

  if (rows.length === 0) {
    display(html`<p style="color:var(--theme-foreground-muted)">No workstream data yet.</p>`);
  } else {
    display(Inputs.table(rows.map((r, i) => ({...r, _ref: i})), {
      columns: ["_ref", "scope_id", "tokens_in", "tokens_out", "tokens_total", "event_count"],
      header: {
        _ref: "REF",
        scope_id: "Workstream",
        tokens_in: "Tokens In",
        tokens_out: "Tokens Out",
        tokens_total: "Total",
        event_count: "Events",
      },
      format: {
        _ref:         (_, i) => refCell(i + 1, "workstreams", rows[i].scope_id),
        scope_id:     d => {
          const ws = wsMap[d];
          return nameCell(ws?.title ?? ws?.slug, d);
        },
        tokens_in:    d => d.toLocaleString(),
        tokens_out:   d => d.toLocaleString(),
        tokens_total: d => d.toLocaleString(),
      },
      width: {_ref: 50, scope_id: 200, tokens_in: 110, tokens_out: 110, tokens_total: 110, event_count: 80},
    }));

    if (total > maxResults)
      display(html`<p style="font-size:0.8rem;color:var(--theme-foreground-muted);margin-top:0.25rem">Showing ${maxResults} of ${total} workstreams</p>`);
  }
}

By Model

if (summary.by_model.length === 0) {
  display(html`<p style="color:var(--theme-foreground-muted)">No model data yet.</p>`);
} else {
  display(Plot.plot({
    title: "Token consumption by model",
    marginLeft: 200,
    width: Math.min(700, width),
    x: {label: "Total tokens", tickFormat: "~s"},
    marks: [
      Plot.barX(summary.by_model, {x: "tokens_total", y: "model", fill: "#4e79a7", tip: true}),
    ],
  }));
}

Top Tasks by Tokens

{
  const sorted = sortRows(taskRowsFull, sortOrder);
  const total  = sorted.length;
  const rows   = sorted.slice(0, maxResults);

  if (rows.length === 0) {
    display(html`<p style="color:var(--theme-foreground-muted)">No task-level data yet.</p>`);
  } else {
    display(Inputs.table(rows.map((r, i) => ({...r, _ref: i})), {
      columns: ["_ref", "task_id", "tokens_in", "tokens_out", "tokens_total"],
      header: {_ref: "REF", task_id: "Task", tokens_in: "In", tokens_out: "Out", tokens_total: "Total"},
      format: {
        _ref:         (_, i) => refCell(i + 1, "tasks", rows[i].task_id),
        task_id:      d => {
          const task = taskMap[d];
          return nameCell(task?.title, d);
        },
        tokens_in:    d => d.toLocaleString(),
        tokens_out:   d => d.toLocaleString(),
        tokens_total: d => d.toLocaleString(),
      },
      width: {_ref: 50, task_id: 240},
    }));

    if (total > maxResults)
      display(html`<p style="font-size:0.8rem;color:var(--theme-foreground-muted);margin-top:0.25rem">Showing ${maxResults} of ${total} tasks</p>`);
  }
}