import { useMemo, useState } from 'react'; import type { Contact, PathDiscoveryResponse, PathDiscoveryRoute } from '../types'; import { findContactsByPrefix, formatForcedRouteSummary, formatLearnedRouteSummary, formatRouteLabel, parsePathHops, } from '../utils/pathUtils'; import { Button } from './ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from './ui/dialog'; interface ContactPathDiscoveryModalProps { open: boolean; onClose: () => void; contact: Contact; contacts: Contact[]; radioName: string | null; onDiscover: (publicKey: string) => Promise; } function formatPathHashMode(mode: number): string { if (mode === 0) return '1-byte hops'; if (mode === 1) return '2-byte hops'; if (mode === 2) return '3-byte hops'; return 'Unknown hop width'; } function renderRouteNodes( route: PathDiscoveryRoute, startLabel: string, endLabel: string, contacts: Contact[] ): string { if (route.path_len <= 0 || !route.path) { return `${startLabel} -> ${endLabel}`; } const hops = parsePathHops(route.path, route.path_len).map((prefix) => { const matches = findContactsByPrefix(prefix, contacts, true); if (matches.length === 1) { return matches[0].name || `${matches[0].public_key.slice(0, prefix.length)}…`; } if (matches.length > 1) { return `${prefix}…?`; } return `${prefix}…`; }); return [startLabel, ...hops, endLabel].join(' -> '); } function RouteCard({ label, route, chain, }: { label: string; route: PathDiscoveryRoute; chain: string; }) { const rawPath = parsePathHops(route.path, route.path_len).join(' -> ') || 'direct'; return (

{label}

{formatRouteLabel(route.path_len, true)}

{chain}

Raw: {rawPath} {formatPathHashMode(route.path_hash_mode)}
); } export function ContactPathDiscoveryModal({ open, onClose, contact, contacts, radioName, onDiscover, }: ContactPathDiscoveryModalProps) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); const learnedRouteSummary = useMemo(() => formatLearnedRouteSummary(contact), [contact]); const forcedRouteSummary = useMemo(() => formatForcedRouteSummary(contact), [contact]); const hasForcedRoute = forcedRouteSummary !== null; const forwardChain = result ? renderRouteNodes( result.forward_path, radioName || 'Local radio', contact.name || contact.public_key.slice(0, 12), contacts ) : null; const returnChain = result ? renderRouteNodes( result.return_path, contact.name || contact.public_key.slice(0, 12), radioName || 'Local radio', contacts ) : null; const handleDiscover = async () => { setLoading(true); setError(null); try { const discovered = await onDiscover(contact.public_key); setResult(discovered); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); } finally { setLoading(false); } }; return ( !isOpen && onClose()}> Path Discovery Send a routed probe to this contact and wait for the round-trip path response. The learned forward route will be saved back onto the contact if a response comes back.
{contact.name || contact.public_key.slice(0, 12)}
Current learned route: {learnedRouteSummary}
{forcedRouteSummary && (
Current forced route: {forcedRouteSummary}
)}
{hasForcedRoute && (
A forced route override is currently set for this contact. Path discovery will update the learned route data, but it will not replace the forced path. Clearing the forced route afterward is enough to make the newly discovered learned path take effect. You only need to rerun path discovery if you want a fresher route sample.
)} {error && (
{error}
)} {result && forwardChain && returnChain && (
)}
); }