generated from coulomb/repo-seed
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:
@@ -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},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user