diff --git a/webui/src/components/CompactNumber.vue b/webui/src/components/CompactNumber.vue index 0429405..4529533 100644 --- a/webui/src/components/CompactNumber.vue +++ b/webui/src/components/CompactNumber.vue @@ -5,13 +5,22 @@ import { formatCompactCount, formatExactInt } from '@/lib/format' /** * CompactNumber renders a large raw token/request count in its compact - * SI-style form ("1.23M") while carrying the exact value via both - * `title` (mouse hover) and `aria-label` (keyboard/screen-reader parity - * — the same established pattern ProviderRateBadge.vue and ModelChip.vue - * already apply to their own hover detail). Used anywhere a usage count - * renders: DataTable numeric columns (usage-columns.ts, target- - * columns.ts, via `h(CompactNumber, { value })`), the Usage tab's Total - * card, and a group accordion's trigger/detail stat grid (UsageView.vue). + * SI-style form ("1.23M") while carrying the exact value via `title` + * (mouse hover) and a visually-hidden sibling span (keyboard/screen- + * reader parity). Used anywhere a usage count renders: DataTable numeric + * columns (usage-columns.ts, target-columns.ts, via + * `h(CompactNumber, { value })`), the Usage tab's Total card, and a + * group accordion's trigger/detail stat grid (UsageView.vue). + * + * Review fix (folded minor, ARIA): `aria-label` on a plain `` is + * an axe `aria-prohibited-attr` violation — a ``'s implicit ARIA + * role ("generic") does not support name computation at all, so + * `aria-label`/`aria-labelledby` there is simply invalid, not merely + * discouraged. The visible, compact text is marked `aria-hidden="true"` + * so assistive tech skips the abbreviated form, and a `sr-only` sibling + * span (visually hidden, still in the accessibility tree) carries the + * exact value instead — the same visible-abbreviation-plus-hidden-full- + * text pattern ProviderRateBadge.vue now applies too. * * Cost figures are NOT this component's concern — formatCost already * shows full 4-decimal precision, so there is nothing to compact or @@ -24,5 +33,8 @@ const exact = computed(() => formatExactInt(props.value)) diff --git a/webui/src/components/ProviderRateBadge.vue b/webui/src/components/ProviderRateBadge.vue index f5aee23..5bdeb7e 100644 --- a/webui/src/components/ProviderRateBadge.vue +++ b/webui/src/components/ProviderRateBadge.vue @@ -37,10 +37,16 @@ const label = computed(() => (status.value === 'no-traffic' ? 'no traffic' : for * detail carries the "right now" (provider) or "today" (model) counts — * the spec's minute-window requirement where that data exists at all * (minuteRateTitle), falling back to dayRateTitle when it does not. Used - * as BOTH the badge's title (mouse hover) and its aria-label (folded - * review minor, v0.22 review round: title alone is mouse-only — a - * keyboard or screen-reader user gets none of it without an aria-label - * carrying the same sentence). + * as BOTH the badge's title (mouse hover) and, via the sr-only span in + * the template, its accessible name for keyboard/screen-reader users + * (folded review minor, v0.22 review round: title alone is mouse-only). + * + * Review fix (v0.23 round): this used to sit directly on `:aria-label` + * on the Badge/`` element, which axe flags as `aria-prohibited- + * attr` — a ``'s implicit role ("generic") does not support name + * computation. See CompactNumber.vue's own doc comment for the identical + * fix and the shared visible-abbreviation-plus-sr-only-full-text + * pattern this component now uses too. */ const detail = computed(() => props.attemptsMinute === undefined || props.failuresMinute === undefined @@ -68,7 +74,8 @@ const extraClass = computed(() => { diff --git a/webui/src/components/ProvidersView.vue b/webui/src/components/ProvidersView.vue index cc33dad..eb49d9b 100644 --- a/webui/src/components/ProvidersView.vue +++ b/webui/src/components/ProvidersView.vue @@ -179,12 +179,18 @@ const aliasColumns: ColumnDef[] = [ id: 'alias', header: 'Alias', accessorFn: (a) => a.alias, + // Optional-chained (review fix, folded minor) the same way + // modelMetaFor guards the provider path below: AdminAliasView. + // modelMeta is typed as always-present, but a real response from a + // server build older than this field (version skew) would otherwise + // throw on `.modelMeta.contextTokens` here instead of just omitting + // the hover detail. cell: ({ row }) => h(ModelChip, { id: row.original.alias, - contextTokens: row.original.modelMeta.contextTokens, - inputPerMTokUsd: row.original.modelMeta.inputPerMTokUsd, - outputPerMTokUsd: row.original.modelMeta.outputPerMTokUsd, + contextTokens: row.original.modelMeta?.contextTokens, + inputPerMTokUsd: row.original.modelMeta?.inputPerMTokUsd, + outputPerMTokUsd: row.original.modelMeta?.outputPerMTokUsd, }), }, { @@ -331,7 +337,8 @@ const aliasEmptyMessage = computed(() => --> {{ formatContextWindow(modelMetaFor(p, m).contextTokens as number) }}