From e39aa0871c16591ce7afbe4919ec062aff1bf9c4 Mon Sep 17 00:00:00 2001 From: whit3rabbit Date: Sun, 5 Apr 2026 15:02:26 -0500 Subject: [PATCH] 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 --- .../admin-ui/src/tabs/traffic/RouteTable.tsx | 39 +++++++++++ .../admin-ui/src/tabs/traffic/TrafficView.tsx | 69 ++++++++++++++++++- .../src/tabs/uptime/BackendHealthRow.tsx | 33 +++++++++ .../admin-ui/src/tabs/uptime/ProxyHealth.tsx | 38 ++++++++++ .../admin-ui/src/tabs/uptime/UptimeView.tsx | 39 ++++++++++- 5 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 crates/proxy/admin-ui/src/tabs/traffic/RouteTable.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/uptime/BackendHealthRow.tsx create mode 100644 crates/proxy/admin-ui/src/tabs/uptime/ProxyHealth.tsx 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 ( + + + + + + + + + + + + + {sorted.map((r) => ( + + + + + + + + + ))} + +
RouteReq/minError rateAvg payloadP95 latencyTotal
{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
+ + + + + + + + + + + + + {data.backends + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map((b) => )} + +
BackendStatusUptime (30d)Last checkedLatencyHistory
+ + )} +
+ ) +}