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:
2026-09-23 16:41:01 +01:00
parent e8abb91dc8
commit ee49467e64
127 changed files with 11226 additions and 3414 deletions
+25
View File
@@ -195,3 +195,28 @@ describe('useEventsStore.startPolling / stopPolling', () => {
}
})
})
// initFromParams was removed (P2 item 10): ReliabilityPage.vue now keeps
// kindFilter/userFilter synced with nav.params via its own reactive
// `watch(() => [nav.params.kind, nav.params.user], ...)`, rather than a
// one-shot mount-time seed — that watch's own doc comment covers why.
describe('useEventsStore.setKindFilter / setUserFilter', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('setKindFilter sets the exact-match kind filter, including back to the "" (every kind) sentinel', () => {
const events = useEventsStore()
events.setKindFilter('timeout')
expect(events.kindFilter).toBe('timeout')
events.setKindFilter('')
expect(events.kindFilter).toBe('')
})
it('setUserFilter sets the raw, un-normalized user/group search text', () => {
const events = useEventsStore()
events.setUserFilter('alice')
expect(events.userFilter).toBe('alice')
})
})