fix(webui): keep charts consistent with the current selection

Chart and ranking fetches carry a request id, and a superseded response
is dropped. Changing a selection clears the old data at once, so old
numbers never show under new labels or units. The auto-refresh tick
skips while a fetch is still in flight. "Loading..." shows only until
the first result for a selection arrives. Dashboard polls do not
overlap.

Users in several groups are counted and searched in every group. The
Models tab labels its windows as the current UTC bucket. Hour labels
are marked UTC. The Members column sorts numerically. Target access
shows "All groups" for null and "No groups" for []. The UI shows the
server's own error message. Cost axis ticks use the currency
formatter. npm run dev proxies /admin/api. Nine unused shadcn
components are removed.

admin_assets_gen.go is regenerated from the new build.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
This commit is contained in:
2026-09-23 09:44:32 +01:00
parent 45b650410d
commit 227200316d
38 changed files with 604 additions and 229 deletions
+20 -2
View File
@@ -3,12 +3,13 @@ import { describe, expect, it } from 'vitest'
import { groupMatches, groupNameMatches, matchingMembersOfGroup, membersOfGroup, userMatches } from './usage-search'
import type { AdminUsageEntryView } from '@/types/api'
/** entry builds a minimal AdminUsageEntryView — only id/groupName matter to this module, the rest are never read. */
function entry(id: string, groupName?: string): AdminUsageEntryView {
/** entry builds a minimal AdminUsageEntryView — only id/groupName/groups matter to this module, the rest are never read. */
function entry(id: string, groupName?: string, groups?: string[]): AdminUsageEntryView {
return {
kind: groupName === undefined ? 'group' : 'user',
id,
groupName,
groups,
requestsPerMinute: 0,
requestsPerDay: 0,
tokensInPerDay: 0,
@@ -52,6 +53,23 @@ describe('membersOfGroup', () => {
expect(membersOfGroup(teamA, users)).toEqual([alice, bob])
expect(membersOfGroup(teamB, users)).toEqual([carol])
})
// Multi-group support (review finding 4): a user's full membership lives
// in `groups` (admin.go's adminUsageEntryView.Groups); groupName alone
// only ever carries the FIRST one. Before this fix, membersOfGroup's
// `groupName === group.id` join silently dropped a multi-group user from
// every group but their first.
it('joins a multi-group user into EVERY one of their member groups via `groups`, not just groupName', () => {
const dave = entry('dave', 'team-a', ['team-a', 'team-b'])
const roster = [alice, bob, carol, dave]
expect(membersOfGroup(teamA, roster)).toEqual([alice, bob, dave])
expect(membersOfGroup(teamB, roster)).toEqual([carol, dave])
})
it('falls back to [groupName] when `groups` is absent (a single-group user, or a fixture predating the field)', () => {
expect(membersOfGroup(teamA, users)).toEqual([alice, bob])
})
})
describe('matchingMembersOfGroup', () => {