/** * Dashboard — the "is it working" page. * * Everything on it is a single `/api/dashboard` fetch for the selected range; * the charts are pure functions of `series`, so they never need their own * polling. The server builds that payload, so every field is read defensively: * a node running an older build simply shows less, it never throws. */ import { useMemo } from 'react' import type { ReactNode } from 'react' import { Area, Bar, BarChart, CartesianGrid, ComposedChart, Line, ResponsiveContainer, Tooltip as ChartTooltip, XAxis, YAxis, } from 'recharts' import { Activity, Ban, Gauge, RefreshCw, Timer } from 'lucide-react' import { getDashboard } from '../lib/api' import type { Dashboard as DashboardData, LogItem, UpstreamStat } from '../lib/types' import { useApi } from '../hooks/useApi' import { useAutoRefresh } from '../hooks/useAutoRefresh' import { TIME_RANGES, useTimeRange } from '../hooks/useTimeRange' import { formatClock, formatCompact, formatMs, formatNumber, formatPercent, formatRate, formatUptime, } from '../lib/format' import { Badge, Button, Card, CardBody, EmptyState, Progress, StatCard, Table } from '../components/ui' import { HealthPill, PageError, PageHeader } from '../components/layout' /** Chart palette: fixed hex so it reads on both the light and dark theme. */ const GRID = 'rgba(148, 163, 184, 0.22)' const AXIS_TEXT = '#94a3b8' const QUERIES = '#38bdf8' const CACHED = '#34d399' const BLOCKED = '#f59e0b' const ERRORS = '#f87171' /** The parts of the dashboard payload that older daemons may not send. */ type DashboardView = Partial & { degraded_reason?: string reason?: string message?: string latency?: { p50_ms?: number; p95_ms?: number; p99_ms?: number } recent?: LogItem[] } type Row = Record function view(data: DashboardData | null): DashboardView { return (data ?? {}) as DashboardView } type TipProps = { active?: boolean label?: number | string payload?: { name?: string; value?: number | string; color?: string }[] } /** Dark tooltip content, so the chart stays legible in the light theme too. */ function ChartTip({ active, payload, label }: TipProps) { if (!active || !payload || payload.length === 0) return null return (
{formatClock(Number(label) * 1000)}
    {payload.map((item) => (
  • ))}
) } function healthTone(health: string | undefined): 'default' | 'ok' | 'warn' | 'danger' { if (health === 'ok') return 'ok' if (health === 'degraded') return 'warn' return 'danger' } function seriesOf(d: DashboardView) { return d.series ?? [] } export default function Dashboard() { const { range, setRange } = useTimeRange('1h') const { data, error, loading, refetch } = useApi(() => getDashboard(range), [range]) useAutoRefresh(refetch, 5000) const d = view(data) const series = useMemo(() => seriesOf(d), [d]) const upstreams = useMemo(() => d.upstreams ?? [], [d]) const slowest = useMemo( () => upstreams.reduce((max, u) => Math.max(max, u.latency_ms || 0), 0), [upstreams], ) const topBlocked = useMemo(() => d.top_blocked ?? [], [d]) const topQueried = useMemo(() => d.top_queried ?? [], [d]) const hitRate = d.cache_hit_rate ?? 0 const p99 = d.latency?.p99_ms const p95Fallback = useMemo(() => { const values = upstreams.map((u) => u.p95_ms || 0).filter((v) => v > 0) return values.length > 0 ? Math.max(...values) : undefined }, [upstreams]) const latencyValue = p99 ?? p95Fallback const latencyLabel = p99 != null ? 'p99 latency' : 'p95 latency' const degraded = d.health != null && d.health !== 'ok' const degradedReason = d.degraded_reason ?? d.reason ?? d.message ?? 'no reason reported by the server' if (error) return const upstreamColumns = [ { key: 'addr', header: 'Server', width: '28%' }, { key: 'proto', header: 'Proto', width: '8%' }, { key: 'health', header: 'Health', width: '12%' }, { key: 'latency', header: 'Latency', width: '22%' }, { key: 'fail', header: 'Fail rate', width: '12%', align: 'right' as const }, { key: 'queries', header: 'Queries', width: '14%', align: 'right' as const }, ] const upstreamRows: Row[] = upstreams.map((u: UpstreamStat, i) => ({ __key: `${u.addr}-${i}`, addr: {u.addr}, proto: {u.proto}, health: {u.health}, latency: ( {formatMs(u.latency_ms)} 0.1 ? 'danger' : u.fail_rate > 0.02 ? 'warn' : 'ok'} /> ), fail: ( {u.fail_rate > 0 ? formatRate(u.fail_rate) : '—'} ), queries: {formatNumber(u.queries)}, })) const recent: LogItem[] = d.recent ?? [] const recentSource: { name: string; list: string; action: string; ts?: string }[] = recent.length > 0 ? recent.slice(0, 10).map((e) => ({ name: e.name, list: e.list, action: e.action, ts: e.ts, })) : topBlocked.slice(0, 10).map((e) => ({ name: e.domain, list: e.list, action: 'block' })) return (
{TIME_RANGES.map((r) => ( ))}
} /> {degraded && (
)}
= 0.7 ? 'ok' : hitRate >= 0.4 ? 'warn' : 'danger'} />

version {d.version || 'unknown'} · up {formatUptime(d.uptime_seconds ?? 0)} {d.node_id ? ` · node ${d.node_id}` : ''} · auto-refreshing every 5s

} cursor={{ stroke: GRID }} />
} cursor={{ stroke: GRID }} />
value.length > 26 ? `${value.slice(0, 25)}…` : value } /> } cursor={{ fill: 'rgba(148,163,184,0.12)' }} />
0 ? 'Newest entries from the server' : 'From the top-blocked ranking'} > {recentSource.length === 0 ? ( ) : (
    {recentSource.map((e, i) => (
  • {e.action || 'block'} {e.name} {e.list ? ( {e.list} ) : null} {e.ts ? ( {formatClock(e.ts)} ) : null}
  • ))}
)}
} >
String(row.__key)} empty="No upstreams are configured." loading={loading && upstreams.length === 0} /> {topQueried.length === 0 ? ( ) : (
    {topQueried.slice(0, 15).map((q) => (
  • {q.qtype || '?'} {q.domain} {formatNumber(q.count)}
  • ))}
)}
) }