diff --git a/crates/proxy/admin-ui/src/tabs/traffic/RouteTable.tsx b/crates/proxy/admin-ui/src/tabs/traffic/RouteTable.tsx
new file mode 100644
index 0000000..4d8c5f1
--- /dev/null
+++ b/crates/proxy/admin-ui/src/tabs/traffic/RouteTable.tsx
@@ -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 (
+
+
+
+ | Route |
+ Req/min |
+ Error rate |
+ Avg payload |
+ P95 latency |
+ Total |
+
+
+
+ {sorted.map((r) => (
+
+ | {r.path} |
+ {r.requests_per_min.toFixed(2)} |
+ 0.05 ? 'var(--err)' : r.error_rate > 0.01 ? 'var(--warn)' : undefined }}>
+ {(r.error_rate * 100).toFixed(1)}%
+ |
+ {formatBytes(r.avg_request_bytes)} |
+ {r.p95_latency_ms}ms |
+ {r.total_requests.toLocaleString()} |
+
+ ))}
+
+
+ )
+}
+
+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`
+}
diff --git a/crates/proxy/admin-ui/src/tabs/traffic/TrafficView.tsx b/crates/proxy/admin-ui/src/tabs/traffic/TrafficView.tsx
index 3ce6d53..2620f03 100644
--- a/crates/proxy/admin-ui/src/tabs/traffic/TrafficView.tsx
+++ b/crates/proxy/admin-ui/src/tabs/traffic/TrafficView.tsx
@@ -1 +1,68 @@
-export default function TrafficView() { return Traffic
}
+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 (
+
+
+ Traffic
+
+
+
+
+
+ {data && (
+ <>
+
+
+
+
+
+
+
Requests / min by route
+
Stacked over time window
+
+
+
+
+
+
+
+
Avg payload per route
+
Bytes
+
+
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/crates/proxy/admin-ui/src/tabs/uptime/BackendHealthRow.tsx b/crates/proxy/admin-ui/src/tabs/uptime/BackendHealthRow.tsx
new file mode 100644
index 0000000..2d85ad1
--- /dev/null
+++ b/crates/proxy/admin-ui/src/tabs/uptime/BackendHealthRow.tsx
@@ -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 (
+
+ | {b.name} |
+
+
+ {b.status}
+ |
+ {b.uptime_pct_30d.toFixed(2)}% |
+ {lastChecked} |
+ {b.last_latency_ms != null ? `${b.last_latency_ms}ms` : '—'} |
+
+
+ {b.history.map((day) => (
+
+ ))}
+
+ |
+
+ )
+}
diff --git a/crates/proxy/admin-ui/src/tabs/uptime/ProxyHealth.tsx b/crates/proxy/admin-ui/src/tabs/uptime/ProxyHealth.tsx
new file mode 100644
index 0000000..1b2f040
--- /dev/null
+++ b/crates/proxy/admin-ui/src/tabs/uptime/ProxyHealth.tsx
@@ -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 (
+
+
+
+
Uptime (30d)
+
{proxy.uptime_pct_30d.toFixed(2)}%
+
+
+
Running
+
{formatDuration(proxy.started_at)}
+
+
+
30-day history
+
+ {proxy.history.map((day) => (
+
+ ))}
+
+
+ )
+}
diff --git a/crates/proxy/admin-ui/src/tabs/uptime/UptimeView.tsx b/crates/proxy/admin-ui/src/tabs/uptime/UptimeView.tsx
index dedab58..f925af7 100644
--- a/crates/proxy/admin-ui/src/tabs/uptime/UptimeView.tsx
+++ b/crates/proxy/admin-ui/src/tabs/uptime/UptimeView.tsx
@@ -1 +1,38 @@
-export default function UptimeView() { return Uptime
}
+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 (
+
+
+ {data && (
+ <>
+
+
Backend Availability
+
+
+
+ | Backend |
+ Status |
+ Uptime (30d) |
+ Last checked |
+ Latency |
+ History |
+
+
+
+ {data.backends
+ .slice()
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((b) => )}
+
+
+ >
+ )}
+
+ )
+}