feat(webui): add limit bars, forecasts, events, exports and URL state

The Usage tab shows a bar for each configured limit. It turns amber at
80% and red at 100%. The tab also shows rejected requests per day, a
projected month-end cost with budget headroom, and CSV export for users
and groups. Names link to Charts with the matching scope.

The Charts tab ranks models over the full 24h, 30d or 12mo window. A
provider filter is applied on the server, and the ranking can be
exported as CSV. A new Events tab lists recent failures, filtered by
kind and user. The selected tab, filters and scope live in the URL
hash. Polling stops while the page is hidden. Views show which replica
answered, and a banner lists configuration warnings.

CSV cells that start with a formula character are neutralised. Links
sit outside the accordion buttons, so both controls keep their
accessible names. admin_assets_gen.go is regenerated.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
This commit is contained in:
2026-09-23 13:09:57 +01:00
parent 25e6a7779e
commit 4261efc41b
53 changed files with 4777 additions and 238 deletions
+23 -4
View File
@@ -12,7 +12,6 @@ import {
TableHead,
TableHeader,
TableRow,
valueUpdater,
} from '@/components/ui/table'
/**
@@ -39,9 +38,24 @@ const props = defineProps<{
rowClass?: (row: TData) => string | undefined
/** Shown in the empty-state row when data is empty. */
emptyMessage?: string
/**
* Optional CONTROLLED sort state (P6, `v-model:sorting`). Omitted (the
* default) keeps every existing call site's original UNCONTROLLED
* behavior unchanged — this component owns `internalSorting` itself and
* a caller has no way to read the current sort. Passed, a caller (e.g.
* UsageView.vue's Users export, which needs to export rows in the
* table's own current sort order — the plan's own "exports
* filtered+sorted rows" requirement) instead owns the ref: every header
* click emits `update:sorting` with the new state rather than mutating
* anything locally here.
*/
sorting?: SortingState
}>()
const sorting = ref<SortingState>([])
const emit = defineEmits<{ 'update:sorting': [value: SortingState] }>()
/** internalSorting is read only when props.sorting is undefined (uncontrolled mode) — see the `sorting` prop's own doc comment above. */
const internalSorting = ref<SortingState>([])
const table = useVueTable({
get data() {
@@ -52,10 +66,15 @@ const table = useVueTable({
},
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: (updater) => valueUpdater(updater, sorting),
onSortingChange: (updater) => {
const current = props.sorting ?? internalSorting.value
const next = typeof updater === 'function' ? updater(current) : updater
if (props.sorting !== undefined) emit('update:sorting', next)
else internalSorting.value = next
},
state: {
get sorting() {
return sorting.value
return props.sorting ?? internalSorting.value
},
},
})