mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-15 05:29:28 +00:00
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.
85 lines
2.7 KiB
Vue
85 lines
2.7 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import UsageTable from '@/components/UsageTable.vue'
|
|
import { formatCost } from '@/lib/format'
|
|
import { useDashboardStore } from '@/stores/dashboard'
|
|
import type { AdminUsageEntryView } from '@/types/api'
|
|
|
|
const dashboard = useDashboardStore()
|
|
const usage = computed(() => dashboard.usage)
|
|
|
|
/** memberCounts maps a group name to its member count — only the Overview response carries it. */
|
|
const memberCounts = computed<Record<string, number>>(() => {
|
|
const out: Record<string, number> = {}
|
|
for (const g of dashboard.overview?.groups ?? []) out[g.name] = g.memberCount
|
|
return out
|
|
})
|
|
|
|
function groupNameOf(entry: AdminUsageEntryView): string {
|
|
return entry.groupName ?? ''
|
|
}
|
|
function memberCountOf(entry: AdminUsageEntryView): string {
|
|
const count = memberCounts.value[entry.id]
|
|
return count === undefined ? '' : String(count)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col gap-6">
|
|
<Card v-if="usage">
|
|
<CardHeader>
|
|
<CardTitle>Total</CardTitle>
|
|
<CardDescription>Usage summed across every user and group combined.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent class="grid grid-cols-2 gap-4 text-sm sm:grid-cols-4">
|
|
<div>
|
|
<p class="text-muted-foreground">Requests/day</p>
|
|
<p class="text-lg font-semibold tabular-nums">{{ usage.total.requestsPerDay }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-muted-foreground">Tokens in/day</p>
|
|
<p class="text-lg font-semibold tabular-nums">{{ usage.total.tokensInPerDay }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-muted-foreground">Tokens out/day</p>
|
|
<p class="text-lg font-semibold tabular-nums">{{ usage.total.tokensOutPerDay }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-muted-foreground">Cost/day</p>
|
|
<p class="text-lg font-semibold tabular-nums">{{ formatCost(usage.total.costPerDayMicroUsd) }}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Groups</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<UsageTable
|
|
id-label="Name"
|
|
secondary-column-label="Members"
|
|
:entries="usage?.groups ?? []"
|
|
:secondary-value="memberCountOf"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Users</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<UsageTable
|
|
id-label="Name"
|
|
secondary-column-label="Group"
|
|
:entries="usage?.users ?? []"
|
|
:secondary-value="groupNameOf"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</template>
|