mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-14 05:18:54 +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.
64 lines
2.3 KiB
Vue
64 lines
2.3 KiB
Vue
<script setup lang="ts">
|
|
import { faCircleExclamation } from '@fortawesome/free-solid-svg-icons'
|
|
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
|
|
|
|
import AuthGate from '@/components/AuthGate.vue'
|
|
import OverviewView from '@/components/OverviewView.vue'
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
import UsageView from '@/components/UsageView.vue'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { useDashboardStore } from '@/stores/dashboard'
|
|
|
|
// Lazy: Chart.js (UsageChart.vue's dependency, pulled in transitively) is
|
|
// the single largest piece of this bundle, and most admin sessions open on
|
|
// Overview/Usage. Splitting it into its own chunk keeps the initial load
|
|
// lean; it fetches once, on first visit to the Charts tab.
|
|
const ChartsView = defineAsyncComponent(() => import('@/components/ChartsView.vue'))
|
|
|
|
const auth = useAuthStore()
|
|
const dashboard = useDashboardStore()
|
|
|
|
const activeView = ref<'overview' | 'usage' | 'charts'>('overview')
|
|
|
|
const statusText = computed<string>(() => {
|
|
if (dashboard.error) return `refresh failed: ${dashboard.error}`
|
|
if (dashboard.lastUpdated) return `last updated ${dashboard.lastUpdated.toLocaleTimeString()}`
|
|
return 'loading...'
|
|
})
|
|
|
|
onMounted(() => {
|
|
dashboard.startPolling()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="mx-auto flex max-w-6xl flex-col gap-6 px-4 py-8">
|
|
<header class="flex flex-col gap-1">
|
|
<h1 class="text-xl font-semibold tracking-tight">LLM Gateway — Admin</h1>
|
|
<p class="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
<FontAwesomeIcon v-if="dashboard.error" :icon="faCircleExclamation" class="size-3.5 text-destructive" />
|
|
<span :class="dashboard.error ? 'text-destructive' : undefined">{{ statusText }}</span>
|
|
</p>
|
|
</header>
|
|
|
|
<AuthGate v-if="!auth.isAuthenticated" />
|
|
|
|
<Tabs v-else v-model="activeView">
|
|
<TabsList>
|
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
<TabsTrigger value="usage">Usage</TabsTrigger>
|
|
<TabsTrigger value="charts">Charts</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="overview">
|
|
<OverviewView />
|
|
</TabsContent>
|
|
<TabsContent value="usage">
|
|
<UsageView />
|
|
</TabsContent>
|
|
<TabsContent value="charts">
|
|
<ChartsView />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</template>
|