import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '../api/client'; import { useModalAmbientDuck } from '../context/AmbientMusicContext'; import { useWebSocket } from '../hooks/useWebSocket'; import type { Agent, PathTraceHop, SpreadRouteRecommendation } from '../types'; import { HelpTip } from '../components/HelpTip'; import SacredPageHeader from '../components/Visual/sacredGeometry/SacredPageHeader'; import './PathTracerPage.css'; // ── types ───────────────────────────────────────────────────────────────────── interface TraceStatus { session_id: string; ready: boolean; error?: string; hops: PathTraceHop[]; spread_routes?: SpreadRouteRecommendation[]; } interface QRData { config: string; qr_png_b64: string; } // ── helpers ─────────────────────────────────────────────────────────────────── function HopStatusBadge({ status }: { status: PathTraceHop['status'] }) { return {status}; } // ── QR Modal ────────────────────────────────────────────────────────────────── function QRModal({ qr, onClose, onEnd, }: { qr: QRData; onClose: () => void; onEnd: () => void; }) { useModalAmbientDuck(true); const [copied, setCopied] = useState(false); const handleCopy = () => { navigator.clipboard.writeText(qr.config).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }; const handleDownload = () => { const blob = new Blob([qr.config], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'pathtrace.conf'; a.click(); URL.revokeObjectURL(url); }; return (
e.target === e.currentTarget && onClose()}>
⬡ PATH TRACE ACTIVE
WireGuard QR

Scan with the WireGuard app on your phone,
or download the .conf file and import it.

{qr.config}
); } // ── main component ──────────────────────────────────────────────────────────── export default function PathTracerPage() { const { agents: wsAgents } = useWebSocket(); const [restAgents, setRestAgents] = useState([]); const [selected, setSelected] = useState([]); // ordered chain const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [sessionID, setSessionID] = useState(''); const [hops, setHops] = useState([]); const [tracing, setTracing] = useState(false); const [qr, setQR] = useState(null); const [showQR, setShowQR] = useState(false); const [spreadRoutes, setSpreadRoutes] = useState([]); const pollRef = useRef | null>(null); const [autoEndCountdown, setAutoEndCountdown] = useState(null); const autoEndRef = useRef | null>(null); // Use WebSocket agents; fall back to REST on mount if WebSocket hasn't populated yet. const agents = wsAgents.length > 0 ? wsAgents : restAgents; useEffect(() => { api.listAgents().then(setRestAgents).catch(() => {}); }, []); // Stop polling and auto-end timer on unmount. useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); if (autoEndRef.current) clearInterval(autoEndRef.current); }, []); const toggleAgent = (id: string, offline: boolean) => { if (offline) return; if (tracing) return; // don't let selection change while tracing setSelected((prev) => { if (prev.includes(id)) return prev.filter((x) => x !== id); if (prev.length >= 3) return prev; // max 3 hops return [...prev, id]; }); }; const handleTrace = useCallback(async () => { if (selected.length === 0) return; setError(''); setLoading(true); setTracing(true); setHops([]); setQR(null); setSpreadRoutes([]); try { const res = await api.startTrace(selected); setSessionID(res.session_id); setHops(res.hops); startPolling(res.session_id); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Trace failed'); setTracing(false); } finally { setLoading(false); } }, [selected]); const startPolling = (sid: string) => { if (pollRef.current) clearInterval(pollRef.current); pollRef.current = setInterval(async () => { try { const status: TraceStatus = await api.getTraceStatus(sid); setHops(status.hops); if (status.spread_routes?.length) { setSpreadRoutes(status.spread_routes); } if (status.error) { setError(status.error); clearInterval(pollRef.current!); pollRef.current = null; setTracing(false); return; } if (status.ready) { try { const qrData = await api.getTraceQR(sid); clearInterval(pollRef.current!); pollRef.current = null; setQR(qrData); setShowQR(true); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Failed to load QR config'); } return; } } catch { // Ignore transient status poll errors } }, 2000); }; const handleEndSession = useCallback(async () => { if (autoEndRef.current) { clearInterval(autoEndRef.current); autoEndRef.current = null; } setAutoEndCountdown(null); if (!sessionID) return; try { await api.deleteTrace(sessionID); } catch { // best-effort } if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } setSessionID(''); setHops([]); setTracing(false); setQR(null); setSelected([]); setSpreadRoutes([]); setError(''); }, [sessionID]); // Auto-delete the session 10 seconds after an error, with a visible countdown. useEffect(() => { if (!error || !sessionID) return; if (autoEndRef.current) clearInterval(autoEndRef.current); const COUNTDOWN = 10; setAutoEndCountdown(COUNTDOWN); let remaining = COUNTDOWN; autoEndRef.current = setInterval(() => { remaining -= 1; if (remaining <= 0) { clearInterval(autoEndRef.current!); autoEndRef.current = null; setAutoEndCountdown(null); handleEndSession(); } else { setAutoEndCountdown(remaining); } }, 1000); return () => { if (autoEndRef.current) { clearInterval(autoEndRef.current); autoEndRef.current = null; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [error, sessionID]); const isWindows = (a: Agent) => !!(a.platform?.toLowerCase().includes('win') || a.platform?.toLowerCase().includes('windows')); const onlineAgents = agents.filter((a) => a.status === 'online'); const offlineAgents = agents.filter((a) => a.status !== 'online'); const allHopsReady = hops.length > 0 && hops.every((h) => h.status === 'ready'); return (
{/* Header */} {error && (
⚠ Session Error
{error}
{sessionID && autoEndCountdown !== null && (
Session will be terminated in {autoEndCountdown}s…
)}
)} {tracing && !allHopsReady && !error && (
Orchestrating tunnel — waiting for agents to configure WireGuard…
)}
{/* Left: agent selection */}
Online agents — click to add to chain (max 3)
{onlineAgents.length === 0 && (
No online agents found.
)}
{onlineAgents.map((a) => { const idx = selected.indexOf(a.id); const isSelected = idx >= 0; const winOnly = isWindows(a); return (
winOnly ? toggleAgent(a.id, false) : undefined} title={!winOnly ? 'WireGuard Path Tracer requires a Windows agent' : undefined} style={!winOnly ? { opacity: 0.5, cursor: 'not-allowed' } : undefined} > {isSelected && ( {idx + 1} )}
{a.name}
{a.ip || '—'}
{!winOnly && (
non-Windows
)}
); })} {offlineAgents.map((a) => (
{a.name}
offline
))}
{/* Right: chain + controls */}
{/* Chain visualizer */}
VPN Chain
{selected.length === 0 ? (
No hops selected yet.
) : (
{/* Phone icon */}
📱 Your Phone
{selected.map((id, i) => { const agent = agents.find((a) => a.id === id); const hop = hops.find((h) => h.agent_id === id); return (
{i + 1}
{agent?.name ?? id.slice(0, 8)} {hop && }
{hop?.external_ip && (
{hop.external_ip}:{hop.port}
)} {hop?.error && (
{hop.error}
)}
); })}
🌐 Internet
)}
{/* Controls */}
{!tracing && !sessionID && ( )} {tracing && allHopsReady && qr && ( )} {(tracing || (!!error && !!sessionID)) && ( )} {!tracing && !sessionID && selected.length > 0 && ( )}
{/* Max hop hint */} {selected.length >= 3 && !tracing && (
Max 3 hops reached.
)} {allHopsReady && (
✓ All hops ready — tunnel is active.
)} {spreadRoutes.length > 0 && (
Spread Routes
    {spreadRoutes.map((route) => (
  • {route.target_subnet} → {route.seed_agent_name ?? route.seed_agent_id.slice(0, 8)} {route.join_lane ? ` (${route.join_lane})` : ''} {route.erasure_lanes_enabled ? ' · RS lanes' : ''} {route.score ? ` · ${route.score.toFixed(2)}` : ''}
  • ))}
)}
{/* QR Modal */} {showQR && qr && ( setShowQR(false)} onEnd={handleEndSession} /> )}
); }