mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-17 05:37:43 +00:00
feat(webui): redesign the admin dashboard around admin tasks
A sidebar replaces the flat tabs with seven pages, and one filter bar sets the time range, comparison period and scope for all of them. - Home: spend against budget, forecast, request rate, error rate, open breakers, top users and models, recent events and warnings. - Spend: spend over time with comparison, budget burn-down, an attribution drill-down from group to user to model, pricing health and cost avoided by free models. - Consumers: a directory with last seen and limit bars, an access matrix, and a detail page per user. - Models: the catalog with billing prices, fleet latency and error rates, and provider health. - Reliability: error, timeout, failover, 429 and 402 trends, events, and latency per provider. - Config: the redacted config and a change helper that emits YAML or users.json snippets to paste into git. - MCP & Agents: callers per target. Page state lives in the URL hash, and old tab links map to the new pages. The replaced views are removed. admin_assets_gen.go is regenerated. Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/api', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/lib/api')>('@/lib/api')
|
||||
return { ...actual, adminFetch: vi.fn() }
|
||||
})
|
||||
|
||||
import { adminFetch, AdminApiError } from '@/lib/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import type { AdminCatalogResponse } from '@/types/api'
|
||||
|
||||
const mockedAdminFetch = vi.mocked(adminFetch)
|
||||
|
||||
const catalogFixture: AdminCatalogResponse = {
|
||||
providers: [
|
||||
{
|
||||
name: 'openai',
|
||||
type: 'openai',
|
||||
lastRefresh: '2026-09-23T00:00:00Z',
|
||||
openUntil: '0001-01-01T00:00:00Z',
|
||||
healthState: 'closed',
|
||||
discoveryEnabled: true,
|
||||
models: [
|
||||
{ id: 'openai/gpt-5', model: 'gpt-5', priceSource: 'builtin', inputPerMTokUsd: 1.25, outputPerMTokUsd: 10 },
|
||||
{ id: 'openai/gpt-5-mini:free', model: 'gpt-5-mini:free', priceSource: 'unpriced', displayFree: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
aliases: [],
|
||||
}
|
||||
|
||||
describe('useCatalogStore.fetch', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockedAdminFetch.mockReset()
|
||||
useAuthStore().submit('test-admin-key')
|
||||
})
|
||||
|
||||
it('is a no-op while unauthenticated', async () => {
|
||||
setActivePinia(createPinia())
|
||||
const catalog = useCatalogStore()
|
||||
await catalog.fetch()
|
||||
expect(mockedAdminFetch).not.toHaveBeenCalled()
|
||||
expect(catalog.data).toBeNull()
|
||||
})
|
||||
|
||||
it('populates data and fetchedAt on success, clearing any prior error', async () => {
|
||||
mockedAdminFetch.mockResolvedValue(catalogFixture)
|
||||
const catalog = useCatalogStore()
|
||||
await catalog.fetch()
|
||||
|
||||
expect(catalog.data).toEqual(catalogFixture)
|
||||
expect(catalog.fetchedAt).not.toBeNull()
|
||||
expect(catalog.error).toBe('')
|
||||
expect(catalog.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('indexes modelsById by canonical id across every provider', async () => {
|
||||
mockedAdminFetch.mockResolvedValue(catalogFixture)
|
||||
const catalog = useCatalogStore()
|
||||
await catalog.fetch()
|
||||
|
||||
expect(catalog.modelsById.get('openai/gpt-5')?.priceSource).toBe('builtin')
|
||||
expect(catalog.modelsById.get('openai/gpt-5-mini:free')?.displayFree).toBe(true)
|
||||
expect(catalog.modelsById.size).toBe(2)
|
||||
})
|
||||
|
||||
it('records a plain error message on a non-auth failure', async () => {
|
||||
mockedAdminFetch.mockRejectedValue(new Error('boom'))
|
||||
const catalog = useCatalogStore()
|
||||
await catalog.fetch()
|
||||
|
||||
expect(catalog.error).toBe('boom')
|
||||
expect(catalog.data).toBeNull()
|
||||
})
|
||||
|
||||
it('does not surface an error for a 401/403 (the auth store owns that case)', async () => {
|
||||
mockedAdminFetch.mockRejectedValue(new AdminApiError('unauthorized', 401))
|
||||
const catalog = useCatalogStore()
|
||||
await catalog.fetch()
|
||||
|
||||
expect(catalog.error).toBe('')
|
||||
})
|
||||
|
||||
it('latest-request-wins: an older in-flight fetch never overwrites a newer one', async () => {
|
||||
let resolveFirst!: (v: AdminCatalogResponse) => void
|
||||
mockedAdminFetch.mockImplementationOnce(
|
||||
() => new Promise<AdminCatalogResponse>((resolve) => (resolveFirst = resolve)),
|
||||
)
|
||||
const catalog = useCatalogStore()
|
||||
const first = catalog.fetch()
|
||||
|
||||
mockedAdminFetch.mockResolvedValueOnce(catalogFixture)
|
||||
await catalog.fetch()
|
||||
expect(catalog.data).toEqual(catalogFixture)
|
||||
|
||||
resolveFirst({ providers: [], aliases: [] })
|
||||
await first
|
||||
// The first (older) fetch's empty result must not clobber the second's.
|
||||
expect(catalog.data).toEqual(catalogFixture)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useCatalogStore.ensureLoaded', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockedAdminFetch.mockReset()
|
||||
mockedAdminFetch.mockResolvedValue(catalogFixture)
|
||||
useAuthStore().submit('test-admin-key')
|
||||
})
|
||||
|
||||
it('fetches when never loaded', async () => {
|
||||
const catalog = useCatalogStore()
|
||||
await catalog.ensureLoaded()
|
||||
expect(mockedAdminFetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not refetch a fresh catalog', async () => {
|
||||
const catalog = useCatalogStore()
|
||||
await catalog.ensureLoaded()
|
||||
await catalog.ensureLoaded()
|
||||
expect(mockedAdminFetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('refetches once stale', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const catalog = useCatalogStore()
|
||||
await catalog.ensureLoaded()
|
||||
vi.advanceTimersByTime(5 * 60 * 1000 + 1)
|
||||
await catalog.ensureLoaded()
|
||||
expect(mockedAdminFetch).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('force bypasses the staleness check', async () => {
|
||||
const catalog = useCatalogStore()
|
||||
await catalog.ensureLoaded()
|
||||
await catalog.ensureLoaded(true)
|
||||
expect(mockedAdminFetch).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user