feat(dashboard): entity list UX — REF column, name cells, detail pages (CUST-WP-0030)

- ref-cell.js: REF column component — click=copy deeplink, dblclick=open
- field-help.js: field registry + fieldRow helper with help-tip decoration;
  FK fields (task_id, workstream_id, repo_id) render as async-linked cells
  with entity-title bubble-help on hover
- GET /token-events/{id} endpoint + get-by-id tests
- GET /repos/by-id/{repo_id} UUID lookup endpoint
- Landing pages: /token-events/[id], /workstreams/[id], /repos/[slug], /tasks/[id]
- token-cost.md: REF + Name columns on all three tables; parallel fetch of
  workstreams/tasks for title resolution
- reference.md: entity detail page URL scheme documented

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 22:35:35 +02:00
parent acb30978cd
commit b28298a2ec
14 changed files with 748 additions and 15 deletions

View File

@@ -4,26 +4,37 @@ title: Token Cost
```js
import {API} from "./components/config.js";
import {refCell} from "./components/ref-cell.js";
const POLL = 60_000;
```
```js
// Fetch both /by-repo/ and raw events in parallel
// Fetch token events, by-repo summary, workstreams, and tasks in parallel
const tokenState = (async function*() {
while (true) {
let byRepo = [], events = [], ok = false;
let byRepo = [], events = [], wsMap = {}, taskMap = {}, ok = false;
try {
const [r1, r2] = await Promise.all([
const [r1, r2, r3, r4] = await Promise.all([
fetch(`${API}/token-events/by-repo/`),
fetch(`${API}/token-events/?limit=1000`),
fetch(`${API}/workstreams/`),
fetch(`${API}/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 {}
yield {byRepo, events, ok, ts: new Date()};
yield {byRepo, events, wsMap, taskMap, ok, ts: new Date()};
await new Promise(res => setTimeout(res, POLL));
}
})();
@@ -58,11 +69,22 @@ function buildSummary(events) {
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;
}
```
```js
const byRepo = tokenState.byRepo ?? [];
const summary = buildSummary(tokenState.events ?? []);
const byRepo = tokenState.byRepo ?? [];
const summary = buildSummary(tokenState.events ?? []);
const wsMap = tokenState.wsMap ?? {};
const taskMap = tokenState.taskMap ?? {};
const _ok = tokenState.ok ?? false;
const _ts = tokenState.ts;
```
@@ -99,6 +121,26 @@ if (byRepo.length === 0) {
),
],
}));
display(Inputs.table(byRepo.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", byRepo[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},
}));
}
```
@@ -109,22 +151,27 @@ const wsRows = summary.by_workstream.slice(0, 20);
if (wsRows.length === 0) {
display(html`<p style="color:var(--theme-foreground-muted)">No workstream data yet.</p>`);
} else {
display(Inputs.table(wsRows, {
columns: ["scope_id", "tokens_in", "tokens_out", "tokens_total", "event_count"],
display(Inputs.table(wsRows.map((r, i) => ({...r, _ref: i})), {
columns: ["_ref", "scope_id", "tokens_in", "tokens_out", "tokens_total", "event_count"],
header: {
scope_id: "Workstream ID",
_ref: "REF",
scope_id: "Workstream",
tokens_in: "Tokens In",
tokens_out: "Tokens Out",
tokens_total: "Total",
event_count: "Events",
},
format: {
scope_id: d => d.slice(0,8) + "…",
_ref: (_, i) => refCell(i + 1, "workstreams", wsRows[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: {scope_id: 120, tokens_in: 110, tokens_out: 110, tokens_total: 110, event_count: 80},
width: {_ref: 50, scope_id: 200, tokens_in: 110, tokens_out: 110, tokens_total: 110, event_count: 80},
}));
}
```
@@ -153,15 +200,20 @@ if (summary.by_model.length === 0) {
if (summary.top_tasks.length === 0) {
display(html`<p style="color:var(--theme-foreground-muted)">No task-level data yet.</p>`);
} else {
display(Inputs.table(summary.top_tasks, {
columns: ["task_id", "tokens_in", "tokens_out", "tokens_total"],
header: {task_id: "Task ID", tokens_in: "In", tokens_out: "Out", tokens_total: "Total"},
display(Inputs.table(summary.top_tasks.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: {
task_id: d => d.slice(0,8) + "…",
_ref: (_, i) => refCell(i + 1, "tasks", summary.top_tasks[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},
}));
}
```