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>
333 lines
14 KiB
Markdown
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.
|