'use client'; import Link from 'next/link'; import { useEffect, useMemo, useState } from 'react'; import { Bar, Section, Stat } from '@/components/ui/primitives'; import { fmt, fmtInt, fmtMs, fmtPct } from '@/lib/format'; import { Time, useTime } from '@/lib/time'; import type { Hop, Probe, RoutePair, RouteResponse, Target } from '@/lib/types'; const HASH_COLORS = ['#5B8DEF', '#E9C46A', '#4CC9F0', '#B497F0', '#F4A261', '#7FB77E', '#E76F51', '#F72585']; function AsnChips({ path, other, label }: { path: number[]; other: number[]; label: string }) { return (
{label} {path.map((asn, i) => { const inOther = other.includes(asn); return ( AS{asn} {i < path.length - 1 && ›} ); })}
); } function HopList({ hops, diffIps, kind, title, total, hash, meta }: { hops: Hop[]; diffIps: Set; kind: 'added' | 'removed'; title: string; total?: number | null; hash: string; meta?: React.ReactNode }) { return (

{title}

{hash.slice(0, 12)} · {hops.length} hops{total != null ? ` · ${fmtMs(total, 1)}` : ''}
{meta &&

{meta}

}
    {hops.map((h) => { const flagged = diffIps.has(h.ip); return (
  1. {h.n} {h.ip} {h.asn != null ? ( AS{h.asn} {h.asn_name} ) : ( {h.private ? 'private' : 'unknown'} )} {flagged && {kind}} {h.rtt_ms == null ? '*' : fmtMs(h.rtt_ms, 1)}
  2. ); })}
); } export function RouteExplorer({ pairs, probes, targets, initialPair, initialRoute }: { pairs: RoutePair[]; probes: Probe[]; targets: Target[]; initialPair: RoutePair | null; initialRoute: RouteResponse | null }) { const [probe, setProbe] = useState(initialPair?.probe_id ?? ''); const [target, setTarget] = useState(initialPair?.target_id ?? ''); const [route, setRoute] = useState(initialRoute); const [err, setErr] = useState<{ key: string; message: string } | null>(null); const pairKey = `${probe}|${target}`; const routeMatches = Boolean(route && route.probe.probe_id === probe && route.target.target_id === target); const errMsg = err && err.key === pairKey ? err.message : null; const loading = Boolean(probe && target) && !routeMatches && !errMsg; const { format } = useTime(); const targetName = useMemo(() => new Map(targets.map((t) => [t.target_id, t.name])), [targets]); const probeName = useMemo(() => new Map(probes.map((p) => [p.probe_id, p.name])), [probes]); const targetsForProbe = useMemo(() => { const ids = new Set(pairs.filter((p) => p.probe_id === probe).map((p) => p.target_id)); return targets.filter((t) => ids.has(t.target_id)); }, [pairs, probe, targets]); const probeIds = useMemo(() => [...new Set(pairs.map((p) => p.probe_id))], [pairs]); useEffect(() => { if (!probe || !target || routeMatches) return; const ctrl = new AbortController(); fetch(`/api/v1/routes?probe=${encodeURIComponent(probe)}&target=${encodeURIComponent(target)}`, { signal: ctrl.signal }) .then((r) => (r.ok ? r.json() : Promise.reject(new Error(r.status === 404 ? 'No traceroute sampled for this pair' : `API ${r.status}`)))) .then((d: RouteResponse) => setRoute(d)) .catch((e: Error) => { if (e.name !== 'AbortError') setErr({ key: `${probe}|${target}`, message: e.message }); }); return () => ctrl.abort(); }, [probe, target, routeMatches]); const hashColor = useMemo(() => { const m = new Map(); const all = [...(route?.route_share_7d.map((r) => r.route_hash) ?? []), ...(route?.history_24h.map((h) => h.route_hash) ?? [])]; for (const h of all) if (!m.has(h)) m.set(h, HASH_COLORS[m.size % HASH_COLORS.length]!); return m; }, [route]); const added = new Set(route?.diff.added.map((h) => h.ip) ?? []); const removed = new Set(route?.diff.removed.map((h) => h.ip) ?? []); return (

{fmtInt(pairs.length)} sampled pairs · {fmtInt(pairs.filter((p) => !p.stable).length)} unstable

{[...pairs] .sort((a, b) => b.changed_24h - a.changed_24h) .slice(0, 10) .map((p) => { const active = p.probe_id === probe && p.target_id === target; return ( ); })}
{errMsg &&

{errMsg}

} {loading &&

Loading traceroute…

} {route && !errMsg && (
changed : unchanged} sub={`hop Δ ${route.diff.hop_delta >= 0 ? '+' : ''}${route.diff.hop_delta}`} /> 5 ? 'var(--p-stressed)' : undefined }}>{`${route.diff.latency_shift_ms >= 0 ? '+' : ''}${fmt(route.diff.latency_shift_ms, 1)} ms`}} />
dominant {fmtPct(route.baseline.share_7d)} of 7 d · first seen {format(route.baseline.first_seen, 'date')} · last seen {format(route.baseline.last_seen, 'short')}} /> sampled {format(route.current.ts, 'short')} · {route.current.reached ? 'destination reached' : 'destination NOT reached'}} />
one cell per traceroute sample · colour = fingerprint} className="mt-6">
{route.history_24h.map((h, i) => ( ))}
{route.history_24h[0] ? {route.history_24h.at(-1) ?
    {route.route_share_7d.map((r) => (
  • ))}
)}
); }