mirror of
https://github.com/whit3rabbit/anyllm-proxy.git
synced 2026-09-22 08:00:51 +00:00
feat(admin-ui): add Traffic and Uptime tabs (route load table, req/min charts, proxy health strip, backend availability)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
b245d22692
commit
e39aa0871c
@@ -0,0 +1,39 @@
|
||||
import type { RouteMetrics } from '../../api/types'
|
||||
|
||||
export default function RouteTable({ routes }: { routes: RouteMetrics[] }) {
|
||||
const sorted = [...routes].sort((a, b) => b.requests_per_min - a.requests_per_min)
|
||||
return (
|
||||
<table className="route-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Route</th>
|
||||
<th>Req/min</th>
|
||||
<th>Error rate</th>
|
||||
<th>Avg payload</th>
|
||||
<th>P95 latency</th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sorted.map((r) => (
|
||||
<tr key={r.path}>
|
||||
<td className="mono">{r.path}</td>
|
||||
<td className="mono">{r.requests_per_min.toFixed(2)}</td>
|
||||
<td className="mono" style={{ color: r.error_rate > 0.05 ? 'var(--err)' : r.error_rate > 0.01 ? 'var(--warn)' : undefined }}>
|
||||
{(r.error_rate * 100).toFixed(1)}%
|
||||
</td>
|
||||
<td className="mono">{formatBytes(r.avg_request_bytes)}</td>
|
||||
<td className="mono">{r.p95_latency_ms}ms</td>
|
||||
<td className="mono">{r.total_requests.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
function formatBytes(n: number) {
|
||||
if (n < 1024) return `${n}B`
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`
|
||||
return `${(n / (1024 * 1024)).toFixed(1)}MB`
|
||||
}
|
||||
@@ -1 +1,68 @@
|
||||
export default function TrafficView() { return <div>Traffic</div> }
|
||||
import { useState } from 'react'
|
||||
import { useTraffic } from '../../api/queries'
|
||||
import RouteTable from './RouteTable'
|
||||
import LineChart from '../../components/shared/LineChart'
|
||||
import EmptyState from '../../components/shared/EmptyState'
|
||||
|
||||
const COLORS = ['#e8a030', '#4caf6e', '#6eb5c0', '#c87dd4', '#e05252']
|
||||
|
||||
export default function TrafficView() {
|
||||
const [windowHours, setWindowHours] = useState(6)
|
||||
const { data, isLoading, error } = useTraffic(windowHours)
|
||||
|
||||
const routes = data?.routes ?? []
|
||||
const series = routes.slice(0, 5).map((r, i) => {
|
||||
const points = (data?.series ?? [])
|
||||
.filter((p) => p.path === r.path)
|
||||
.map((p) => p.requests)
|
||||
return { label: r.path, color: COLORS[i % COLORS.length], data: points }
|
||||
})
|
||||
|
||||
const payloadSeries = routes.slice(0, 5).map((r, i) => ({
|
||||
label: r.path,
|
||||
color: COLORS[i % COLORS.length],
|
||||
data: [r.avg_request_bytes],
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="section-header">
|
||||
<span className="section-label">Traffic</span>
|
||||
<select value={windowHours} onChange={(e) => setWindowHours(Number(e.target.value))}>
|
||||
<option value={1}>Last 1 hour</option>
|
||||
<option value={6}>Last 6 hours</option>
|
||||
<option value={24}>Last 24 hours</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<EmptyState loading={isLoading} error={error?.message} />
|
||||
|
||||
{data && (
|
||||
<>
|
||||
<RouteTable routes={data.routes} />
|
||||
|
||||
<div className="operator-grid" style={{ marginTop: 16 }}>
|
||||
<div className="chart-card">
|
||||
<div className="chart-header">
|
||||
<div>
|
||||
<div className="chart-title">Requests / min by route</div>
|
||||
<div className="chart-subtitle">Stacked over time window</div>
|
||||
</div>
|
||||
</div>
|
||||
<LineChart series={series} />
|
||||
</div>
|
||||
<div className="chart-card">
|
||||
<div className="chart-header">
|
||||
<div>
|
||||
<div className="chart-title">Avg payload per route</div>
|
||||
<div className="chart-subtitle">Bytes</div>
|
||||
</div>
|
||||
</div>
|
||||
<LineChart series={payloadSeries} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { BackendUptimeInfo } from '../../api/types'
|
||||
import StatusDot from '../../components/shared/StatusDot'
|
||||
|
||||
export default function BackendHealthRow({ b }: { b: BackendUptimeInfo }) {
|
||||
const dotStatus: 'ok' | 'err' | 'dim' = b.status === 'up' ? 'ok' : b.status === 'down' ? 'err' : 'dim'
|
||||
const lastChecked = b.last_checked_at
|
||||
? new Date(b.last_checked_at * 1000).toLocaleTimeString()
|
||||
: '—'
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="mono">{b.name}</td>
|
||||
<td>
|
||||
<StatusDot status={dotStatus} pulse={b.status === 'up'} />
|
||||
{b.status}
|
||||
</td>
|
||||
<td className="mono">{b.uptime_pct_30d.toFixed(2)}%</td>
|
||||
<td className="mono dim">{lastChecked}</td>
|
||||
<td className="mono dim">{b.last_latency_ms != null ? `${b.last_latency_ms}ms` : '—'}</td>
|
||||
<td>
|
||||
<div className="history-bar" style={{ height: 12 }}>
|
||||
{b.history.map((day) => (
|
||||
<div
|
||||
key={day.date}
|
||||
className={`history-day ${day.status}`}
|
||||
title={`${day.date}: ${day.status}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ProxyUptimeInfo } from '../../api/types'
|
||||
|
||||
function formatDuration(startedAt: number) {
|
||||
const secs = Math.floor(Date.now() / 1000 - startedAt)
|
||||
const d = Math.floor(secs / 86400)
|
||||
const h = Math.floor((secs % 86400) / 3600)
|
||||
const m = Math.floor((secs % 3600) / 60)
|
||||
if (d > 0) return `${d}d ${h}h ${m}m`
|
||||
if (h > 0) return `${h}h ${m}m`
|
||||
return `${m}m`
|
||||
}
|
||||
|
||||
export default function ProxyHealth({ proxy }: { proxy: ProxyUptimeInfo }) {
|
||||
return (
|
||||
<div className="uptime-proxy">
|
||||
<div className="uptime-proxy-stats">
|
||||
<div>
|
||||
<div className="section-label">Uptime (30d)</div>
|
||||
<div className="uptime-pct">{proxy.uptime_pct_30d.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="section-label">Running</div>
|
||||
<div className="stat-value" style={{ fontSize: 16 }}>{formatDuration(proxy.started_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section-label" style={{ marginBottom: 4 }}>30-day history</div>
|
||||
<div className="history-bar">
|
||||
{proxy.history.map((day) => (
|
||||
<div
|
||||
key={day.date}
|
||||
className={`history-day ${day.status}`}
|
||||
title={`${day.date}: ${day.status}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +1,38 @@
|
||||
export default function UptimeView() { return <div>Uptime</div> }
|
||||
import { useUptime } from '../../api/queries'
|
||||
import ProxyHealth from './ProxyHealth'
|
||||
import BackendHealthRow from './BackendHealthRow'
|
||||
import EmptyState from '../../components/shared/EmptyState'
|
||||
|
||||
export default function UptimeView() {
|
||||
const { data, isLoading, error } = useUptime()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EmptyState loading={isLoading} error={error?.message} />
|
||||
{data && (
|
||||
<>
|
||||
<ProxyHealth proxy={data.proxy} />
|
||||
<div className="section-label" style={{ marginTop: 16, marginBottom: 8 }}>Backend Availability</div>
|
||||
<table className="backend-health-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Backend</th>
|
||||
<th>Status</th>
|
||||
<th>Uptime (30d)</th>
|
||||
<th>Last checked</th>
|
||||
<th>Latency</th>
|
||||
<th>History</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.backends
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((b) => <BackendHealthRow key={b.name} b={b} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user