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
Add a "Filter users or groups" search input to UsageView, mirroring
OverviewView's model search (same Input component, icon, and
clear-button styling). Case-insensitive, trimmed substring match:
- A user matches when its own name contains the query.
- A group matches when its own name contains the query OR it has at
least one matching member.
Groups accordion shows only matching groups, sorted through the
existing Groups toolbar unchanged. A group that matches only via a
member auto-expands (using the same tested override semantics as
Overview's provider search); a group matched by its own name is not
force-expanded, since the match is already visible on its collapsed
trigger row. The nested members table inside an expanded group shows
every member when the group matched by name, and only the matching
members when it matched via a member.
The flat Users table filters to matching users, plus every member of
a name-matched group. The Total card and ChartsView's chart rendering
are untouched by the filter.
ChartsView's scope picker (Select) gets the identical search input,
filtering the user/group option list it offers. "Total" always stays
in the list, and the currently selected scope is always kept in the
list even if a new query would otherwise hide it, so an active chart
is never torn down or re-labeled by narrowing the picker.
Generalize lib/provider-expand.ts into lib/search-expand.ts: the same
two-set (manuallyExpanded/manuallyCollapsed) auto-expand/override state
machine now drives both Overview's providers and Usage's groups, keyed
on generic string ids instead of provider names specifically.
OverviewView.vue is updated to the renamed
computeExpandedItems/toggleItemExpand exports; its own behavior is
unchanged. The moved spec (search-expand.spec.ts) keeps all 9 original
cases plus 2 new ones pinning the same semantics against non-provider
(group-shaped) ids.
Add lib/usage-search.ts with the shared user/group matching helpers
(userMatches, groupNameMatches, membersOfGroup,
matchingMembersOfGroup, groupMatches) so UsageView and ChartsView
share one matching implementation, with its own vitest spec.
UsageTable.vue gains an optional emptyMessage prop (threaded to
DataTable) so UsageView can show a "no users match ..."/"no groups
match ..." message under an active filter, matching Overview's own
alias-search empty-state pattern.
No backend/Go logic changes; filtering is client-side over the
already-polled GET /admin/api/usage response.
Component-first admin-panel rework (operator directive), replacing
hand-rolled patterns with the actual shadcn-vue library components
rather than working around them.
- Providers (OverviewView) and Groups (UsageView) now render as a real
shadcn-vue Accordion: trigger is the summary line, content holds the
detail (base URL, last refresh/error, model chips for providers;
full stat grid + member users for groups) — the earlier hand-rolled
second-<TableRow> toggle is retired. lib/provider-expand.ts's tested
two-set expand-state module is unchanged; only the template now
adapts it to Accordion's type="multiple" v-model via a writable
computed instead of driving a plain v-if.
- Usage view: expanding a group shows its member users' own usage rows
— a client-side join on AdminUsageEntryView.groupName, which admin.go
already sets per user (verified via admin_test.go's existing
TestAdminUsage_MathAgainstSeededCounters; no backend change needed).
- New DataTable.vue: the shadcn-vue DataTable pattern
(@tanstack/vue-table, pinned to ^8.21.3 — the version whose API
matches the documented pattern; npm's default "latest" resolves to
v9, a breaking rewrite with a different API) factored into one
reusable, generic component so every real table (users usage,
aliases, a group's nested member list) gets clickable sortable
headers with asc/desc indicators for free. usage-columns.ts holds
the shared ColumnDef set; UsageTable.vue now delegates to DataTable.
Groups themselves cannot be a literal <table> (Accordion wraps
content in a <div>, illegal inside <tbody>), so their own
sortability is driven by the same columns headlessly, surfaced as a
small header-button toolbar above the Accordion.
- Aliases table: alias id gets the ModelChip copy treatment (the alias
name is the routable string); target stays plain text.
- App.vue: dropped the max-w-6xl container — the panel now uses the
full viewport width with responsive gutters (px-4 sm:px-6 lg:px-8).
Table.vue already wraps every table in its own overflow-x-auto
container, so a wide table still scrolls itself, never the page.
shadcn-vue add accordion re-added @lucide/vue and required swapping
its Chevron icons for FontAwesome (webui/README.md's own documented
trap); components.json's iconLibrary/font keys stayed absent
(verified main.css byte-identical after the add).
Browser-verified against the real integration stack (real /admin over
the Yaegi plugin, strict CSP incl. style-src 'self'): provider/group
accordion expand, search auto-expand through the new Accordion
adapter, sort toggles on aliases/users/groups-toolbar/nested-member
tables, alias copy-to-clipboard, full-width at 1920px. Zero console
errors (one unrelated MetaMask extension log).
Vue 3 + TypeScript + Pinia + Tailwind v4 + shadcn-vue + Chart.js admin
panel source under webui/, built with Vite. Views: Overview (providers,
redis/cache/retry status, model aliases), Usage (per-user/per-group/total
tables with tokensIn/tokensOut/cost + limits), Charts (scope selector,
req/tokens(stacked in-out)/cost metric tabs, 24h/30d/12mo window
switcher, fed by GET /admin/api/usage/history). Pinia stores: auth
(sessionStorage key gate, x-api-key header, 401/403 re-prompt),
dashboard (5s poll), history (fetch-on-selection + 30s refresh).
Design: deliberate neutral warm-neutral palette with a signature teal
accent (no default Inter/purple-gradient/glassmorphism), real
typographic hierarchy via the platform UI/mono font stacks (zero
external assets - no Google Fonts), dark+light via prefers-color-scheme
(no class toggle, no theme switcher). Charts: zero-based bars, honest
axes, Okabe-Ito colorblind-safe series, tokens-in/out as a
distinguishable stacked pair.
vite.config.ts: base '/admin/' (matches admin.go's route layout) and
build.modulePreload.polyfill:false (no inline bootstrap script, so the
tightened admin CSP needs no 'unsafe-inline'). Code-split: Chart.js
(ChartsView) loads lazily, not in the initial bundle.
generate.mjs reads dist/ after `vite build` and writes
../admin_assets_gen.go (not committed by this change) - base64-encodes
every asset, decoded once at package-var init; documented rationale in
the script's own header comment.
Hygiene: webui/go.mod walls webui/ off from the repo root's `go build`/
`vet`/`test ./...` (npm's node_modules bundles a stray .go file that
would otherwise be swept in); tools/yaegi-check/main.go excludes webui/
from its GOPATH copy for the same reason. webui/ export-ignored
(.gitattributes) from release source tarballs; webui/node_modules/
gitignored. tsconfig.app.json/tsconfig.node.json's JSONC comments
dropped (TS itself is fine with either; the global pre-commit JSON
validator is not) - no config values changed.