dashboard-panel
Add, change or remove a panel on a console dashboard — the dashboards.yaml entry, its i18n keys, the Storybook story, the request-count test and the Typst template. Use whenever a task mentions a dash
SKILL.md.claude/skills/dashboard-panel/SKILL.md
GitHubAdding or changing a dashboard panel
A dashboard is a page entry in apps/console/dashboards.yaml, not a container. You almost never
write a React component for a new panel — you write YAML, two locale keys, and a test number.
Read first: docs/knowledge/dashboards.md (the contract) and ADR 0015 D1/D2/D2b (the reasoning).
The sequence
1. Edit apps/console/dashboards.yaml
Find the page by route:. Add the panel to panels:.
- id: <page-prefix>-<what-it-reads> # unique DOCUMENT-wide, not just page-wide
type: ranked # one of DASHBOARD_PANEL_TYPES
title: <page-key>.<panel-id>.title # an i18n KEY, never prose
subtitle: <page-key>.<panel-id>.subtitle # optional
span: 1 # 1 or 2
metric: cost # cost | requests | tokens | latency | derived:<name>
options: { topN: 6, link: '/admin/usage/actors/:key?type=$lens' }
query:
scope: all
group_by: [model]
bucket: auto
limit: 2000 # REQUIRED — never omit
Non-negotiables:
limitis always explicit. A response the backend truncated must render a caption naming that number, not a quietly short chart.- Prefix the id per page (
actor-*,channel-*,chat-*). The id is the React key, the dedupe attribution, the heading id, the?<panel-id>-scale=URL knob and the report's lookup. - Reuse an existing query shape if you can. Panels sharing a fully-resolved query share ONE
request. Order
group_byso your panel's own dimension is first, or setoptions.dimension. $placeholdermust be a filter the page declares.$name?is legal only insidefilters.<key>— never onscope/scope_id.- A ring (
donut) is a decision, not a default. Three exist, named in ADR 0015 D2. A per-key breakdown isranked. options.style: stacked-barsis sanctioned for three panels only (daily spend x model). Every otherseriespanel stays lines.
2. Add the locale keys — BOTH files
apps/console/locales/en/dashboards.json and apps/console/locales/de/dashboards.json. The
client ships only the active locale, so a missing de key renders as a raw key, not English.
Keys needed: title, and any of subtitle, options.rowLabel, options.unit you set.
3. Update the page's request-count test
Every dashboard page pins panel count and resolved request count. Adding a panel moves at least one.
| Page | Test |
|---|---|
/admin/usage |
apps/console/src/dashboards/admin-usage-page.test.ts (also pins the 19 ids IN ORDER) |
/admin/overview |
apps/console/src/dashboards/admin-overview-page.test.ts |
the three /admin/usage drill-downs |
apps/console/src/dashboards/admin-usage-detail-pages.test.ts |
| the account and settings overviews | apps/console/src/dashboards/overview-pages.test.ts |
Update the number and say in the PR body why the shape changed. Never delete the assertion. If the count did NOT go up, that is the dedupe working — state that too.
4. The story needs nothing
packages/ui-web/src/pages-stories/spec-page.tsx imports the real dashboards.yaml and the real
locales/en/dashboards.json. Your panel appears in Dashboard/FromSpec automatically. If it renders
with a visible i18n key, step 2 is incomplete.
If you added a new options.* knob, thread it through SpecPanel in spec-page.tsx and add a case
to apps/console/src/dashboards/spec-page-story-parity.test.tsx — the story and the console must not
disagree about style, topN or link.
5. Templates: only for a NEW route
An existing route's .typ template iterates report.panels and picks the new one up. A new
page entry needs nothing either — templates/_lib/default.typ handles it. Add
apps/console/templates/<route>/report.typ only when the document needs its own chrome. See the
report-template skill.
Verify — all four, with real output
pnpm --filter console test # the spec + page tests
pnpm --filter console typecheck
pnpm --filter @lightbridge/ui-web test
pnpm --filter console build:web # the REAL Next build (not `build`)
Then look at it: pnpm --filter @lightbridge/ui-web storybook → Dashboard/FromSpec. See the
console-story-verify skill.
Pitfalls that have actually bitten
pnpm --filter console builddoes not exist. The script isbuild:web, and it runsscripts/build-report-charts.mjsfirst. Rootpnpm build(turbo) also works.- A YAML typo is a STARTUP failure, not a compile error.
formatDashboardIssuesnames the page and the panel id. Read the whole message before guessing. - A new panel type needs a renderer in the same change.
DASHBOARD_PANEL_TYPES(packages/ui-web/src/sections/dashboard-panels/types.ts) andpanelRenderers(.../panel-renderers.tsx) must cover each other exactly — a test asserts it. - Options set in YAML but not threaded into the story oracle silently diverge. That is what
spec-page-story-parity.test.tsxexists for; it was added after a real divergence. - A
stat/stat-grouppanel gets no Card and no Expand. That isSELF_PANELLING_TYPES, not a bug to fix. - Never add a legend list under a chart. Values live on hover (Floating UI tooltip). Standing owner ruling.
- Do not add a caption that claims something the data cannot support. Read the honest-captions
list in
docs/knowledge/admin-area.mdbefore writing a subtitle.