Files
the-custodian/workplans/CUST-WP-0030-dashboard-entity-list-ux.md
tegwick f5c166e77e feat(dashboard): add repo filter, sort order, and max results controls to Token Cost page
Three reactive dropdowns below the Token Cost heading:
- Filter by repo: client-side filter via 3-level chain resolution
- Sort by: Tokens Total (default), Tokens In, Out, Event Count, Most Recent
- Show: 10/20/50/100/500 rows per table (default 20)

Applies uniformly to By Repo, By Workplan, and Top Tasks tables.
"Most Recent" derives last_event_at per group from the fetched events.
Truncated tables show a "Showing M of N" count below.

Completes CUST-WP-0030 T07–T09.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 00:02:17 +02:00

333 lines
14 KiB
Markdown

---
id: CUST-WP-0030
type: workplan
title: "Dashboard Entity List UX"
domain: custodian
repo: the-custodian
status: done
owner: custodian
topic_slug: custodian
created: "2026-03-29"
updated: "2026-03-29"
state_hub_workstream_id: "9d8e1c33-2067-4593-a5d8-d28dda3b1d21"
---
# Dashboard Entity List UX
## Goal
Make every entity table in the dashboard navigable and self-documenting.
Two new UI primitives:
1. **REF cell** — running row number (1-based); click copies a deep-link
(`<origin>/data/<recordtype>/<id>`) to the clipboard; double-click opens
the link in a new tab.
2. **Name cell** — second column, titled after the record type; shows the
entity name truncated at 80 chars with full-name tooltip on hover.
Support these cells with a landing page (`/data/[type]/[id]`) that renders
a key-value view of every field in the record, each key decorated with a
`<help-tip>` (one-sentence description + link to the relevant help-doc
section).
Pilot all three features on the **Token Cost** page, then extend to other
entity tables in later workplans.
## Background
Current entity tables show only IDs (often truncated UUIDs) with no way to
navigate to a record detail view or discover what a field means. For a
person reviewing agent activity the tables are hard to parse. Providing a
running reference number, an entity name column, and a click-through detail
page substantially lowers the cognitive load.
The `<help-tip>` custom element already exists in
`src/components/help-tip.js` and is used on several pages — the field-help
registry will reuse it as the rendering layer.
## Implementation Plan
The pilot targets the Token Cost page. Observable Framework supports
parameterised pages via `[param].md` file naming. The API already exposes
`GET /token-events/?task_id=…` and `GET /token-events/summary/`; a
per-record GET endpoint is needed before the landing page can work.
### Component layer
- `src/components/ref-cell.js` — exports `refCell(index, type, id)` that
returns an `HTMLElement` with click/dblclick handlers.
- `src/components/field-help.js` — exports `FIELD_HELP` map and
`fieldRow(key, value)` helper that wraps the key in `<help-tip>`.
### API layer
- `GET /token-events/{event_id}` — returns `TokenEventRead` for a single
event; needed by the landing page data loader.
### Dashboard pages
- `src/data/token-events/[id].json.py` — data loader that calls the new
single-event endpoint; returns JSON for the landing page.
- `src/data/token-events/[id].md` — landing page: key-value table, each
row built with `fieldRow()`.
- `src/token-cost.md` — updated: REF column (refCell) and Name column
added to By Repo, By Workplan, and Top Tasks tables.
## Tasks
```task
id: T01
title: "REF cell component"
status: done
priority: high
description: >
Create src/components/ref-cell.js.
Export refCell(index, recordType, id) → HTMLElement.
- Displays 1-based index as monospace text, cursor:pointer.
- Single click: copies `${location.origin}/data/${recordType}/${id}` to
clipboard; briefly flashes "Copied!" as a transient tooltip using the
existing help-tip positioning pattern.
- Double click: window.open(deeplink, '_blank').
- No external dependencies; plain DOM.
state_hub_task_id: "8ee527cf-436b-4bab-bdb8-406314a38d99"
```
```task
id: T02
title: "GET /token-events/{event_id} endpoint"
status: done
priority: high
description: >
Add GET /token-events/{event_id} to api/routers/token_events.py.
Returns TokenEventRead (already defined in schemas/token_event.py).
404 if not found.
No migration needed.
Add a test in tests/test_token_events.py: get by id → 200, unknown id → 404.
state_hub_task_id: "02c27d25-d744-4da0-9bcb-b40ada54d5a5"
```
```task
id: T03
title: "Field-help registry"
status: done
priority: medium
description: >
Create src/components/field-help.js.
Export FIELD_HELP: a plain object keyed by field name.
Each entry: { label, description, doc } — matches help-tip attributes.
Cover at minimum all TokenEventRead fields:
id, tokens_in, tokens_out, tokens_total, task_id, workstream_id,
repo_id, session_id, model, agent, ref_type, ref_id, note, created_at.
Export fieldRow(key, value) → HTMLElement (<tr>) that wraps key in a
<help-tip> (or plain <td> if key is not in FIELD_HELP) and value in a
second <td>.
Import HelpTip from ./help-tip.js to ensure the custom element is
registered.
state_hub_task_id: "7721d884-bd70-459f-b36d-450d69aac549"
```
```task
id: T04
title: "Token-event landing page"
status: done
priority: medium
description: >
Create two files:
1. src/data/token-events/[id].json.py — data loader.
Reads `id` from argv[1]; calls GET /token-events/{id};
exits 1 if 404 so Observable renders an error page.
2. src/data/token-events/[id].md — landing page.
Imports fieldRow from components/field-help.js.
Fetches FileAttachment("token-events/{id}.json").json().
Renders an HTML <table> with one row per field using fieldRow().
Title: "Token Event · {id.slice(0,8)}…".
Back link: "<a href='/token-cost'>← Token Cost</a>".
state_hub_task_id: "dc63746d-74b3-434a-925c-1cead480198f"
```
```task
id: T05
title: "Apply REF and Name columns to Token Cost page"
status: done
priority: high
description: >
Update src/token-cost.md.
Import refCell from ./components/ref-cell.js.
For each of the three entity tables (By Repo, By Workplan, Top Tasks):
- Prepend a "REF" column using refCell(i+1, recordType, row.id).
Record types: "repos" for By Repo (using repo_id), "workstreams" for
By Workplan (using scope_id), "token-events" for Top Tasks (using
task_id — note: links to the task landing page, not a token event page,
until T04 is done; use recordType "tasks").
- Add a Name column as the second data column:
- By Repo: repo_slug (no truncation needed, slugs are short)
- By Workplan: scope_id displayed as first 8 chars + "…" (unchanged)
→ replace with workstream title if available; for now show scope_id
truncated to 36 chars with full UUID tooltip.
- Top Tasks: task_id truncated to 80 chars with full-id tooltip.
Keep all existing columns unchanged.
state_hub_task_id: "3225cc6c-2574-41e9-b8fd-e5e703a9dd7c"
```
```task
id: T07
title: "Repo filter dropdown on Token Cost page"
status: done
priority: high
description: >
Add a "Filter by repo" select element directly below the Token Cost page
heading (above all three tables). Populate options from the already-fetched
/repos/ data (plus an "All repos" default at the top). When a repo is selected,
re-render all three tables (By Repo, By Workplan, Top Tasks) showing only rows
whose data is attributed to that repo. The individual-events data (fetched for
wsMap/taskMap) is already keyed by repo_id on each event — use that to filter
By Workplan and Top Tasks rows client-side. By Repo always shows at most the
selected repo (one row). If "All repos" is selected, all rows are shown as
before. Implement filtering as a reactive variable that triggers table redraws.
state_hub_task_id: "c01cead4-ff9c-4533-b3d8-9f7554387771"
```
```task
id: T08
title: "Sort order dropdown on Token Cost page"
status: done
priority: high
description: >
Add a "Sort by" select element immediately to the right of the repo filter
dropdown. Options (in display order):
"Tokens Total" (default — current server-side order)
"Tokens In"
"Tokens Out"
"Event Count"
"Most Recent"
Implement as a client-side sort applied after filtering and before slicing for
max-results. For the first four options sort descending by the named field.
For "Most Recent", sort each table's rows by the most recent created_at among
the individual token events belonging to that row's group (repo/workstream/task).
Derive a lastEventAt lookup map from the already-fetched /token-events/ data;
rows with no events sort last. The sort applies uniformly to all three tables.
state_hub_task_id: "84183245-5016-4d87-ad6a-9cd5f6873245"
```
```task
id: T09
title: "Max results dropdown on Token Cost page"
status: done
priority: medium
description: >
Add a "Show" select element immediately to the right of the sort dropdown.
Options: 10, 20, 50, 100, 500. Default: 20.
After filtering and sorting each table's data array, slice to at most N rows
before rendering. Display the total available count beneath or beside each
table when the table is truncated (e.g. "Showing 20 of 47"). The limit applies
independently per table (each table may have different totals). No API change
needed — client-side slice of the already-fetched arrays.
state_hub_task_id: "3ef43135-fb65-4cca-b8c3-4c7eeb52107c"
```
```task
id: T06
title: "Consistency gate and docs update"
status: done
priority: low
description: >
1. Run `cd state-hub && make test` — all token_events tests must pass.
2. Run `make fix-consistency REPO=the-custodian`.
3. Add a one-paragraph entry to src/docs/reference.md describing the
/data/<type>/<id> URL scheme and the REF column convention.
state_hub_task_id: "107cb5bb-ff0c-4c97-af04-2cdeff11f0b2"
```
## Amendments & Improvements
Post-completion fixes and enhancements discovered during manual testing.
### A01 — amendment — Deep-link URL prefix wrong
`ref-cell.js` generated deep-links as `<origin>/data/<type>/<id>` but
Observable Framework serves pages at `/<type>/<id>` (the `src/data/`
directory is for data loaders, not pages). Fixed by removing the `/data/`
prefix from the `deepLink` construction in `ref-cell.js`. The T01
description was also inaccurate in stating the `/data/` path.
### A02 — amendment — FileAttachment rejects template literals
T04 used `` FileAttachment(`../data/token-events/${eventId}.json`) `` in
the landing page. Observable Framework requires `FileAttachment` to be
called with a single **literal** string (it processes them statically at
compile time). This caused a `SyntaxError` that also aborted the cell
defining `eventId`, cascading into a `RuntimeError: wsId is not defined`
on the next cell. Fixed by replacing all `FileAttachment` calls in landing
pages with direct `fetch(${API}/...)` calls.
### A03 — amendment — Workstream and repo landing pages missing
T04 only created a token-event landing page. The By Workplan and By Repo
tables also had REF links (to `/workstreams/<id>` and `/repos/<slug>`),
both returning 404. Added:
- `src/workstreams/[id].md` — fetches `GET /workstreams/{id}`
- `src/repos/[slug].md` — fetches `GET /repos/{slug}/`
- Corresponding unused data loaders left in `src/data/` for reference.
### A04 — amendment — Repos router uses slug, not UUID
T05 passed `repo_id` (UUID) to `refCell` for the By Repo table, but the
repos API uses slug-based routing (`GET /repos/{slug}/`). Passing a UUID
returned 404. Fixed by passing `repo_slug` to `refCell` so the deep-link
resolves correctly.
### A05 — amendment — Top Tasks refCell used wrong record type
T05 specified `"token-events"` as the record type for Top Tasks, but the
column contains `task_id` (a task UUID, not a token-event UUID), so the
landing page returned "Token event not found". Fixed by:
- Changing record type to `"tasks"` in the `refCell` call.
- Creating `src/tasks/[id].md` (fetches `GET /tasks/{task_id}`).
### I02 — improvement — Entity FK fields on detail pages link to their targets
On every detail page (token-event, task, workstream, repo), fields that hold
a foreign-key UUID (`task_id`, `workstream_id`, `repo_id`) now render as
clickable links with an async-loaded bubble-help showing the entity title.
Implementation:
- `GET /repos/by-id/{repo_id}` added to `api/routers/repos.py` — UUID
lookup needed because the existing repos router uses slug routing.
- `FIELD_LINKS` registry added to `src/components/field-help.js` mapping
each FK field to `{apiUrl, getUrl, getTitle}` resolution rules.
`getUrl` receives `(id, data)` so slug-routed entities can derive their
page URL from the fetched entity (e.g. `repo_id` → `/repos/{data.slug}`).
- `_linkCell(key, id)` helper added: renders a short-UUID link immediately,
then fetches the entity asynchronously and wraps the anchor in a
`<help-tip label="{title}" description="{type} · {full-uuid}">` once
the data arrives.
- `fieldRow` updated to dispatch to `_linkCell` whenever the field key is
in `FIELD_LINKS` and the value is non-null.
### I05 — improvement — Max results dropdown
A "Show" select (10 / 20 / 50 / 100 / 500, default 20) sits to the right of
the sort dropdown. After filtering and sorting, each table is sliced to at most
N rows. A "Showing M of N" note appears below any truncated table.
### I04 — improvement — Sort order dropdown
A "Sort by" select sits to the right of the repo filter. Options: Tokens Total
(default), Tokens In, Tokens Out, Event Count, Most Recent. Sorting is applied
client-side after filtering and before the max-results slice. "Most Recent"
sorts by the maximum `created_at` among the events in each group, derived from
the already-fetched individual events data.
### I03 — improvement — Repo filter dropdown
A "Filter by repo" select appears directly below the Token Cost heading. Options
come from the already-fetched `/repos/` list plus "All repos" at the top.
Selecting a repo filters all three tables client-side to show only rows
attributable to that repo. No API change needed.
### I01 — improvement — Workstream and task Name columns show titles
T05 originally showed truncated UUIDs in the Workstream and Task name
columns (the summary data carries only IDs, not titles). Improved by
fetching `/workstreams/` and `/tasks/` in parallel with the token-event
poll and building lookup maps (`wsMap`, `taskMap`). The Name column now
displays `workstream.title` and `task.title` (truncated to 80 chars) with
the full UUID as tooltip.