feat(webui): compact usage-count formatting

Operator scope addition: raw token/request counts reaching millions/
billions were unreadable as bare digits. formatCompactCount (lib/
format.ts, shipped in the previous commit alongside this feature's own
helpers) renders an SI-style label: below 10,000 the exact integer with
thousands separators, at or above it three significant figures with a
k/M/B suffix ("12.3k", "456k", "1.23M", "45.6M", "1.23B") —
every digit past the third significant one is FLOORED, never rounded,
the same honesty rule formatRatePercent already applies to the
success-rate badge: a count of 999,950 must read "999k", never
"1,000k", since it has not actually crossed that boundary.

CompactNumber.vue is the one shared, reusable presentational component
(component-first: no per-call-site wrapper markup) — renders the
compact form as text, with the exact value on both title and aria-label,
mirroring ProviderRateBadge.vue/ModelChip.vue's own hover-parity
pattern.

Applied everywhere a count renders: usage-columns.ts's numericColumn
(reqMin/reqDay/tokIn-out per day and month — cost columns explicitly
opt out via a new compact:false argument, since formatCost already
shows full precision), target-columns.ts's MCP/agent request counters,
UsageView.vue's Total card and group-accordion trigger/detail stat
grid, and UsageChart.vue's Chart.js y-axis ticks (compact only — no
room for the raw figure on an axis label) and tooltip labels (compact
form WITH the precise raw value alongside, e.g. "Tokens in: 1.23M
(1,234,567)").

Sorting is unaffected by construction: every numericColumn's accessorFn
still returns the raw number (what getSortedRowModel actually sorts by)
regardless of what the cell renders — usage-columns.spec.ts pins this
directly, including a sort-order assertion that would fail under a
lexicographic (string) sort but passes under the numeric one.
This commit is contained in:
2026-08-22 01:36:48 +01:00
parent eac0862d68
commit 040ff40ae0
6 changed files with 181 additions and 22 deletions
+12 -7
View File
@@ -1,16 +1,21 @@
import type { ColumnDef } from '@tanstack/vue-table'
import { h } from 'vue'
import CompactNumber from '@/components/CompactNumber.vue'
import { Badge } from '@/components/ui/badge'
import type { AdminTargetView } from '@/types/api'
/**
* numericColumn builds one right-aligned, sortable request-counter column.
* Mirrors usage-columns.ts's own helper of the same name and purpose, but
* without that one's storeDown '?' masking: a target's counters carry no
* storeDown flag (limiter.targetUsage, limits.go — a target scope has no
* limit to protect from a misleadingly-confident zero, unlike a limited
* user/group scope), so there is nothing to mask here.
* numericColumn builds one right-aligned, sortable request-counter
* column, rendered via CompactNumber (SI-style, exact value on title/
* aria-label — usage-columns.ts's own numericColumn applies the
* identical treatment). Mirrors usage-columns.ts's own helper of the
* same name and purpose, but without that one's storeDown '?' masking: a
* target's counters carry no storeDown flag (limiter.targetUsage,
* limits.go — a target scope has no limit to protect from a
* misleadingly-confident zero, unlike a limited user/group scope), so
* there is nothing to mask here. accessorFn (and therefore sorting)
* always reads the true raw number.
*/
function numericColumn(id: string, header: string, read: (t: AdminTargetView) => number): ColumnDef<AdminTargetView, unknown> {
return {
@@ -18,7 +23,7 @@ function numericColumn(id: string, header: string, read: (t: AdminTargetView) =>
header,
accessorFn: read,
meta: { align: 'right' },
cell: ({ row }) => String(read(row.original)),
cell: ({ row }) => h(CompactNumber, { value: read(row.original) }),
}
}