feat(admin-ui): add app shell (main, App, LoginPage, Nav) and tab stubs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
whit3rabbit
2026-04-05 14:45:41 -05:00
co-authored by Claude Sonnet 4.6
parent 49a1292c0f
commit acda35f063
13 changed files with 199 additions and 0 deletions
+62
View File
@@ -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<Tab>('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 <LoginPage />
return (
<div>
<Nav activeTab={activeTab} onTabChange={setActiveTab} />
<div className="tab-content">
{activeTab === 'dashboard' && <Dashboard />}
{activeTab === 'requests' && <RequestLog />}
{activeTab === 'settings' && <Settings />}
{activeTab === 'backends' && <Backends />}
{activeTab === 'keys' && <Keys />}
{activeTab === 'models' && <Models />}
{activeTab === 'audit' && <Audit />}
{activeTab === 'traffic' && <TrafficView />}
{activeTab === 'uptime' && <UptimeView />}
</div>
</div>
)
}
@@ -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<HTMLFormElement>) {
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 (
<div className="login-overlay">
<div className="login-card">
<div className="login-title">
<span className="prompt">&gt;&nbsp;</span>proxy admin
</div>
<form onSubmit={handleSubmit}>
<input
type="password"
name="token"
placeholder="Admin token"
autoComplete="current-password"
autoFocus
/>
<button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? 'Signing in\u2026' : 'Sign in'}
</button>
</form>
<div className="login-error">{error}</div>
</div>
</div>
)
}
@@ -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 (
<nav className="nav">
<div className="nav-brand">anyllm</div>
{TABS.map((t) => (
<div
key={t.id}
className={`nav-item${activeTab === t.id ? ' active' : ''}`}
onClick={() => onTabChange(t.id)}
>
{t.label}
</div>
))}
<div className="nav-right">
<span
className={`ws-status ${wsStatus === 'connected' ? 'connected' : 'disconnected'}`}
>
{wsStatus === 'connected' ? 'Live' : 'Offline'}
</span>
<button
className="btn btn-secondary btn-sm"
style={{ marginLeft: 12 }}
onClick={logout}
>
Sign out
</button>
</div>
</nav>
)
}
+22
View File
@@ -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(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>,
)
@@ -0,0 +1 @@
export default function Audit() { return <div>Audit</div> }
@@ -0,0 +1 @@
export default function Backends() { return <div>Backends</div> }
@@ -0,0 +1 @@
export default function Dashboard() { return <div>Dashboard</div> }
@@ -0,0 +1 @@
export default function Keys() { return <div>Keys</div> }
@@ -0,0 +1 @@
export default function Models() { return <div>Models</div> }
@@ -0,0 +1 @@
export default function RequestLog() { return <div>Request Log</div> }
@@ -0,0 +1 @@
export default function Settings() { return <div>Settings</div> }
@@ -0,0 +1 @@
export default function TrafficView() { return <div>Traffic</div> }
@@ -0,0 +1 @@
export default function UptimeView() { return <div>Uptime</div> }