Files
lukaszraczylo b081055aaa fix(admin): wire-level access tests, MCP/agent UI, glob-safe chips
Review fix round on the group-access-display feature (Should-Fix + folded
items):

1. TestAdminUsage_GroupAccessLists now asserts at the raw-JSON level, not
   just the typed adminUsageResponse struct: decodes rows into
   map[string]json.RawMessage and checks the four access-list keys are
   ENTIRELY ABSENT on user/total rows and an unrestricted group, PRESENT
   with exact values on a restricted group. A typed-struct decode alone
   cannot prove omitempty is doing its job -- a present-but-null field
   decodes into the same nil []string a genuinely absent key would.
   Verified by temporarily stripping the omitempty tags: the new
   assertions fail (four keys read back 'null'); the old struct-only
   assertions stayed green throughout, confirming the gap the review
   flagged.

2. UsageView.vue's Access block now renders MCP servers and Agents too,
   same Badge treatment as Providers, with muted 'All MCP servers'/
   'All agents' fallbacks -- GroupConfig carries all four lists
   (llmgateway.go), so the display now matches the payload instead of
   dropping two of the four fields silently.

3. A configured Models entry containing *, ?, or [ (path.Match's own
   special characters -- auth.go's matchesGlob) is a pattern, not a
   routable id: it now renders as a plain, non-copyable Badge
   (font-mono, secondary) instead of the copyable ModelChip, so a caller
   can no longer copy a glob pattern into a request's model field
   thinking it's a real id. Every Access label (Providers/Models/MCP
   servers/Agents) also gets a title attribute (one shared GLOB_HINT
   constant) making the glob-match semantics discoverable, since all
   four lists are glob-matched, not just Models.

4. groupSummary's providers/models/mcpServers/agents are now independent
   copies (cloneStringSlice), not aliases of the live group.* slices
   authorization reads on every request -- structurally prevents a
   future display-side sort/mutation from ever reordering or corrupting
   enforcement data. slices.Clone was tried first and reverted: it broke
   make yaegi-check (yaegi's stdlib symbol table does not resolve the
   generic slices package's type parameters under interpretation), so
   cloneStringSlice uses plain make/copy builtins instead -- verified
   yaegi-check passes with this version.

5. README.md's usage endpoint section: fixed the stale 'Overview and
   Usage views' reference (now 'Providers', matching the earlier tab
   rename) and added one line naming the new providers/models/
   mcpServers/agents group-access fields, matching the section's
   existing terseness.

6. Renamed the 'unrestricted group carries empty/omitted lists' subtest
   to 'empty or omitted' -- the slash broke -run subtest path matching.

7. The Access block now uses the same dl/dt/dd markup as the sibling
   stat grid immediately below it, instead of a bespoke div/p structure
   -- one convention per panel.
2026-08-21 13:41:11 +01:00
..
2026-08-21 09:21:41 +01:00
2026-08-21 09:21:41 +01:00
2026-08-21 09:21:41 +01:00

Admin panel (webui)

Source for the traefik-llmgateway read-only admin dashboard: Vue 3 + TypeScript + Pinia + Tailwind v4 + shadcn-vue + Chart.js, built with Vite.

See the repo root README.md's Admin section for what the dashboard shows and how its API/auth works, and the Development section for make admin-ui — the only way this directory's output (../admin_assets_gen.go) is meant to be regenerated. Nothing here runs at request time: the plugin serves the built, baked-in output, never webui/ itself.

Warning: shadcn-vue add/init regressions

components.json has no iconLibrary or font key. Do not add them back. Their omission is the fix, not an oversight — read on before you "helpfully" restore either one.

Earlier revisions of this file set "iconLibrary": "lucide" and "font": "inter", both wrong for this project (FontAwesome only, per the vue-web-development house skill; no external font at all). Both npx shadcn-vue init and npx shadcn-vue add <component> read those two fields and, when set, rewrite src/assets/main.css — hit twice in this project's own history, init then a later add, both re-adding a @import url('https://fonts.googleapis.com/...') line and switching the dark-mode @custom-variant back to a .dark-class toggle.

The actual shadcn-vue config schema (node_modules/shadcn-vue/dist/schema/index.js's rawConfigSchema) declares both fields as z.string().optional() — not an enum, contrary to an earlier, incorrect claim in this project's own review notes that the CLI schema forced one of five fixed values. The CLI's init --font/ --icon-library flags do offer five fixed choices each, but that is a flag-parsing constraint, not a components.json schema constraint. Deleting both keys leaves them undefined after parse, a valid state: verified empirically, running npx shadcn-vue add badge -o -y against a components.json with both keys removed left src/assets/main.css byte-for-byte unchanged.

That verification also surfaced a narrower, remaining trap: the same run still added @lucide/vue back to package.json's dependencies (Badge itself does not use it — some components' registry entries list an icon package regardless of iconLibrary). So after any future shadcn-vue add/init call:

  1. Check git diff webui/package.json for a re-added lucide-vue-next/@lucide/vue dependency, and remove it (npm uninstall) if nothing in src/ imports from it.
  2. Check the new component's own file for a lucide-vue-next/@lucide/vue import and swap it for the FontAwesome equivalent (see src/components/ui/select/Select*.vue for the pattern already applied there).
  3. Diff src/assets/main.css anyway before committing, in case a future shadcn-vue release starts reading a different config key for the same font-injection step.

Local development

npm ci
npm run dev      # Vite dev server — /admin/api/* calls need a real
                  # gateway to fetch from; point one at the same origin or
                  # accept the "refresh failed" state while iterating on
                  # layout/styling alone.
npm run build     # what `make admin-ui` runs, plus webui/generate.mjs

Structure

  • src/stores/ — Pinia: auth (the sessionStorage key gate), dashboard (Providers/Usage, 5s poll), history (Charts, fetch-on-selection + 30s refresh).
  • src/components/ — AuthGate, ProvidersView (the "Providers" tab — named for its UI label; it still fetches GET /admin/api/overview, whose own name is unchanged API surface), UsageView, ChartsView/UsageChart, UsageTable, DataTable (the shared sortable-table wrapper — shadcn-vue's DataTable pattern, @tanstack/ vue-table, generic over row type), SearchInput (the shared search-box — icon + shadcn Input + shadcn Button clear button — every search filter in the panel renders); ui/ holds the shadcn-vue primitives this panel actually uses (Accordion, Card, Table, Tabs, Button, Input, Alert, Select, Badge) — nothing installed and unused. Providers and Groups (Usage) render as a real Accordion — expanding a group shows its member users' own usage rows, a client-side join on AdminUsageEntryView.groupName.
  • src/composables/ — useThemeColors (Chart.js axis/grid/legend colors off the current design tokens), useSearchQuery (the shared query/normalized/hasQuery state every SearchInput pairs with).
  • src/lib/ — api.ts (the /admin/api/* fetch wrapper), format.ts, key-storage.ts, chart-setup.ts, search-expand.ts (search-aware expand state, adapted to drive Accordion's v-model — shared by ProvidersView's providers and UsageView's groups), usage-search.ts (shared user/group query-matching helpers, used by UsageView and ChartsView), usage-columns.ts (shared ColumnDefs for every usage table).
  • src/types/api.ts — TypeScript mirrors of admin.go's JSON response shapes (adminOverviewResponse, adminUsageResponse, usageHistoryResponse) — these keep their backend names; only the frontend tab label/component changed (see ProvidersView above).
  • generate.mjs — reads dist/ after vite build and writes ../admin_assets_gen.go; see its own header comment for why every asset is base64-encoded rather than embedded as a raw string literal.