From acda35f0637dfb91f464fb1cec7bc7d1a4529523 Mon Sep 17 00:00:00 2001 From: whit3rabbit Date: Sun, 5 Apr 2026 14:45:41 -0500 Subject: [PATCH] feat(admin-ui): add app shell (main, App, LoginPage, Nav) and tab stubs Co-Authored-By: Claude Sonnet 4.6 --- crates/proxy/admin-ui/src/App.tsx | 62 +++++++++++++++++++ .../src/components/layout/LoginPage.tsx | 51 +++++++++++++++ .../admin-ui/src/components/layout/Nav.tsx | 55 ++++++++++++++++ crates/proxy/admin-ui/src/main.tsx | 22 +++++++ .../proxy/admin-ui/src/tabs/audit/Audit.tsx | 1 + .../admin-ui/src/tabs/backends/Backends.tsx | 1 + .../admin-ui/src/tabs/dashboard/Dashboard.tsx | 1 + crates/proxy/admin-ui/src/tabs/keys/Keys.tsx | 1 + .../proxy/admin-ui/src/tabs/models/Models.tsx | 1 + .../admin-ui/src/tabs/requests/RequestLog.tsx | 1 + .../admin-ui/src/tabs/settings/Settings.tsx | 1 + .../admin-ui/src/tabs/traffic/TrafficView.tsx | 1 + .../admin-ui/src/tabs/uptime/UptimeView.tsx | 1 + 13 files changed, 199 insertions(+) create mode 100644 crates/proxy/admin-ui/src/App.tsx create mode 100644 crates/proxy/admin-ui/src/components/layout/LoginPage.tsx create mode 100644 crates/proxy/admin-ui/src/components/layout/Nav.tsx create mode 100644 crates/proxy/admin-ui/src/main.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/audit/Audit.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/backends/Backends.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/dashboard/Dashboard.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/keys/Keys.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/models/Models.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/requests/RequestLog.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/settings/Settings.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/traffic/TrafficView.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/uptime/UptimeView.tsx diff --git a/crates/proxy/admin-ui/src/App.tsx b/crates/proxy/admin-ui/src/App.tsx new file mode 100644 index 0000000..6fcb030 --- /dev/null +++ b/crates/proxy/admin-ui/src/App.tsx @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react' +import { useQueryClient } from '@tanstack/react-query' +import { useAuthStore } from './store/auth' +import { useWsStore } from './store/ws' +import { connectWs, disconnectWs } from './api/websocket' +import LoginPage from './components/layout/LoginPage' +import Nav from './components/layout/Nav' +import Dashboard from './tabs/dashboard/Dashboard' +import RequestLog from './tabs/requests/RequestLog' +import Settings from './tabs/settings/Settings' +import Backends from './tabs/backends/Backends' +import Keys from './tabs/keys/Keys' +import Models from './tabs/models/Models' +import Audit from './tabs/audit/Audit' +import TrafficView from './tabs/traffic/TrafficView' +import UptimeView from './tabs/uptime/UptimeView' + +type Tab = 'dashboard' | 'requests' | 'settings' | 'backends' | 'keys' | 'models' | 'audit' | 'traffic' | 'uptime' + +export default function App() { + const token = useAuthStore((s) => s.token) + const lastEvent = useWsStore((s) => s.lastEvent) + const qc = useQueryClient() + const [activeTab, setActiveTab] = useState('dashboard') + + useEffect(() => { + if (token) { + connectWs() + } else { + disconnectWs() + } + }, [token]) + + // Invalidate query cache on relevant WS events. + useEffect(() => { + if (!lastEvent) return + if (lastEvent.type === 'metrics_snapshot') { + qc.setQueryData(['metrics'], lastEvent.data) + } else if (lastEvent.type === 'backend_health_changed') { + qc.invalidateQueries({ queryKey: ['uptime'] }) + } + }, [lastEvent, qc]) + + if (!token) return + + return ( +
+
+ ) +} diff --git a/crates/proxy/admin-ui/src/components/layout/LoginPage.tsx b/crates/proxy/admin-ui/src/components/layout/LoginPage.tsx new file mode 100644 index 0000000..3b3ef0e --- /dev/null +++ b/crates/proxy/admin-ui/src/components/layout/LoginPage.tsx @@ -0,0 +1,51 @@ +import { useState, type FormEvent } from 'react' +import { useAuthStore } from '../../store/auth' + +export default function LoginPage() { + const login = useAuthStore((s) => s.login) + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + async function handleSubmit(e: FormEvent) { + e.preventDefault() + const token = (e.currentTarget.elements.namedItem('token') as HTMLInputElement).value.trim() + if (!token) return + setLoading(true) + setError('') + try { + // Validate by hitting a lightweight endpoint with the candidate token. + const res = await fetch('/admin/api/metrics', { + headers: { Authorization: `Bearer ${token}` }, + }) + if (!res.ok) throw new Error('Invalid token') + login(token) + } catch { + setError('Invalid token') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+ proxy admin +
+
+ + +
+
{error}
+
+
+ ) +} diff --git a/crates/proxy/admin-ui/src/components/layout/Nav.tsx b/crates/proxy/admin-ui/src/components/layout/Nav.tsx new file mode 100644 index 0000000..fb21741 --- /dev/null +++ b/crates/proxy/admin-ui/src/components/layout/Nav.tsx @@ -0,0 +1,55 @@ +import { useAuthStore } from '../../store/auth' +import { useWsStore } from '../../store/ws' + +type Tab = 'dashboard' | 'requests' | 'settings' | 'backends' | 'keys' | 'models' | 'audit' | 'traffic' | 'uptime' + +const TABS: { id: Tab; label: string }[] = [ + { id: 'dashboard', label: 'Dashboard' }, + { id: 'requests', label: 'Request Log' }, + { id: 'settings', label: 'Settings' }, + { id: 'backends', label: 'Backends' }, + { id: 'keys', label: 'Access Control' }, + { id: 'models', label: 'Models' }, + { id: 'audit', label: 'Audit' }, + { id: 'traffic', label: 'Traffic' }, + { id: 'uptime', label: 'Uptime' }, +] + +interface NavProps { + activeTab: Tab + onTabChange: (tab: Tab) => void +} + +export default function Nav({ activeTab, onTabChange }: NavProps) { + const logout = useAuthStore((s) => s.logout) + const wsStatus = useWsStore((s) => s.status) + + return ( + + ) +} diff --git a/crates/proxy/admin-ui/src/main.tsx b/crates/proxy/admin-ui/src/main.tsx new file mode 100644 index 0000000..35fcd9f --- /dev/null +++ b/crates/proxy/admin-ui/src/main.tsx @@ -0,0 +1,22 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import App from './App' +import './styles/globals.css' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/crates/proxy/admin-ui/src/tabs/audit/Audit.tsx b/crates/proxy/admin-ui/src/tabs/audit/Audit.tsx new file mode 100644 index 0000000..62e133d --- /dev/null +++ b/crates/proxy/admin-ui/src/tabs/audit/Audit.tsx @@ -0,0 +1 @@ +export default function Audit() { return
Audit
} diff --git a/crates/proxy/admin-ui/src/tabs/backends/Backends.tsx b/crates/proxy/admin-ui/src/tabs/backends/Backends.tsx new file mode 100644 index 0000000..42c92e6 --- /dev/null +++ b/crates/proxy/admin-ui/src/tabs/backends/Backends.tsx @@ -0,0 +1 @@ +export default function Backends() { return
Backends
} diff --git a/crates/proxy/admin-ui/src/tabs/dashboard/Dashboard.tsx b/crates/proxy/admin-ui/src/tabs/dashboard/Dashboard.tsx new file mode 100644 index 0000000..8b0e14f --- /dev/null +++ b/crates/proxy/admin-ui/src/tabs/dashboard/Dashboard.tsx @@ -0,0 +1 @@ +export default function Dashboard() { return
Dashboard
} diff --git a/crates/proxy/admin-ui/src/tabs/keys/Keys.tsx b/crates/proxy/admin-ui/src/tabs/keys/Keys.tsx new file mode 100644 index 0000000..822a952 --- /dev/null +++ b/crates/proxy/admin-ui/src/tabs/keys/Keys.tsx @@ -0,0 +1 @@ +export default function Keys() { return
Keys
} diff --git a/crates/proxy/admin-ui/src/tabs/models/Models.tsx b/crates/proxy/admin-ui/src/tabs/models/Models.tsx new file mode 100644 index 0000000..54828e6 --- /dev/null +++ b/crates/proxy/admin-ui/src/tabs/models/Models.tsx @@ -0,0 +1 @@ +export default function Models() { return
Models
} diff --git a/crates/proxy/admin-ui/src/tabs/requests/RequestLog.tsx b/crates/proxy/admin-ui/src/tabs/requests/RequestLog.tsx new file mode 100644 index 0000000..aaa22c8 --- /dev/null +++ b/crates/proxy/admin-ui/src/tabs/requests/RequestLog.tsx @@ -0,0 +1 @@ +export default function RequestLog() { return
Request Log
} diff --git a/crates/proxy/admin-ui/src/tabs/settings/Settings.tsx b/crates/proxy/admin-ui/src/tabs/settings/Settings.tsx new file mode 100644 index 0000000..e3d0162 --- /dev/null +++ b/crates/proxy/admin-ui/src/tabs/settings/Settings.tsx @@ -0,0 +1 @@ +export default function Settings() { return
Settings
} diff --git a/crates/proxy/admin-ui/src/tabs/traffic/TrafficView.tsx b/crates/proxy/admin-ui/src/tabs/traffic/TrafficView.tsx new file mode 100644 index 0000000..3ce6d53 --- /dev/null +++ b/crates/proxy/admin-ui/src/tabs/traffic/TrafficView.tsx @@ -0,0 +1 @@ +export default function TrafficView() { return
Traffic
} diff --git a/crates/proxy/admin-ui/src/tabs/uptime/UptimeView.tsx b/crates/proxy/admin-ui/src/tabs/uptime/UptimeView.tsx new file mode 100644 index 0000000..dedab58 --- /dev/null +++ b/crates/proxy/admin-ui/src/tabs/uptime/UptimeView.tsx @@ -0,0 +1 @@ +export default function UptimeView() { return
Uptime
}