From bf6e2114452f900d29e2b41221e3b0b60f90f728 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 04:09:17 +0000 Subject: [PATCH] Add Edge Path Explorer: measure the path instead of simulating it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Route Model draws a plausible great-circle path with generated intermediate hops, because a browser cannot send ICMP or set an IP TTL. Rather than keep improving a simulation, this adds a tool built only on things the browser can actually observe. Four real measurements: - Connection phase breakdown from the Performance Timeline: DNS, TCP, TLS, time-to-first-byte and transfer, per origin. Two failure modes are detected and reported rather than rendered as zeros. Cross-origin responses without a Timing-Allow-Origin header have every phase timestamp zeroed by spec, and a reused connection has no handshake to measure at all — both would otherwise read as "0 ms". - Which CDN edge answered, via speed.cloudflare.com/meta and, for Cloudflare-fronted hosts, /cdn-cgi/trace. The IATA colo code resolves against a bundled airport table (~170 entries) to a real coordinate; an unknown code is flagged rather than guessed. - HTTP/3 negotiation across four h3-capable origins. If all of them fall back to HTTP/2, the browser tried QUIC and failed, which is direct evidence that UDP/443 is blocked upstream. - Latency horizon: light travels ~200 km/ms in fibre, so a round trip puts a hard ceiling on distance. Drawn as a constraint ring — the endpoint is somewhere inside it. Queuing delay only loosens the bound, so this is a proof rather than an estimate. The Cloudflare meta call also replaces three separate third-party GeoIP providers for client identity, and the answer comes from the network element actually handling the traffic rather than a lookup database. New EdgeMap component draws only points with verifiable coordinates, and builds all popup content as DOM nodes with textContent — the same treatment applied to the route map after the XSS fix. The Route Model is kept, relabelled "SIM" in the nav and demoted from the dashboard hero, so existing history still renders. Also discloses the three new probe origins in the privacy statement and README, adds edgepath to the CSV/ZIP export with an Availability column so a blank phase is never read as zero, and adds geoip and edgepath to the history type filters. Verified with 24 new unit tests over the phase parser, trace parser, colo resolution, distance bound and protocol classifier, plus a Chromium run of both paths: 7/7 checks on the failure path (no fabricated timings when the probes cannot be reached) and 9/9 on the success path with the network stubbed, confirming correct TCP/TLS separation, colo resolution, distance calculation, map markers and constraint rings. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Dz4sWqBnBqN12tDaDn6b8D --- README.md | 24 +- src/App.tsx | 5 + src/components/Dashboard.tsx | 39 +- src/components/EdgeMap.tsx | 243 ++++++++++++ src/components/EdgePathExplorer.tsx | 514 ++++++++++++++++++++++++++ src/components/HistoryLog.tsx | 2 +- src/components/Navbar.tsx | 5 +- src/components/PrivacySafetyModal.tsx | 5 + src/data/iata.ts | 222 +++++++++++ src/types.ts | 143 ++++++- src/utils/edgePath.test.ts | 282 ++++++++++++++ src/utils/edgePath.ts | 506 +++++++++++++++++++++++++ src/utils/export.ts | 84 ++++- 13 files changed, 2057 insertions(+), 17 deletions(-) create mode 100644 src/components/EdgeMap.tsx create mode 100644 src/components/EdgePathExplorer.tsx create mode 100644 src/data/iata.ts create mode 100644 src/utils/edgePath.test.ts create mode 100644 src/utils/edgePath.ts diff --git a/README.md b/README.md index 758d58a..72dcd21 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,30 @@ Geolocation, ISP, ASN and proxy/VPN signals for an IP or domain, via third-party ### 11. 📊 Live Traffic Monitor Real-time throughput and latency from the browser's own Performance Timeline. -### 12. 🗺️ Route Model *(simulated — read this)* +### 12. 🧭 Edge Path Explorer +Everything a browser can genuinely observe about the path to a host: + +- **Connection phase breakdown** — real DNS → TCP → TLS → time-to-first-byte → transfer timings + from the Performance Timeline. Cross-origin phases require a `Timing-Allow-Origin` header, and + handshake phases only exist on a connection's *first* request; both conditions are detected and + reported rather than shown as zeros. +- **Which CDN edge answered**, by IATA code, resolved against a bundled airport table to a real + coordinate. An unknown code is flagged, not guessed. +- **HTTP/3 negotiation** — if every h3-capable origin falls back to HTTP/2, that is direct evidence + UDP/443 is blocked upstream by a firewall or middlebox. +- **Latency horizon** — light travels ~200 km/ms in fibre, so a round trip puts a hard ceiling on + how far away a server can be. Drawn as a constraint circle: the endpoint is somewhere inside it. + This is a proof, not an estimate — queuing delay only loosens the bound. + +### 13. 🗺️ Route Model *(simulated — read this)* Resolves a target, looks up its real location, and draws a plausible great-circle path to it. **The intermediate hops are generated, not measured.** Browsers cannot send ICMP packets or set an IP TTL, so no web page can perform a real traceroute. The first and last hops are grounded in a real DNS resolution and a real geolocation lookup; everything between them is illustrative. Exports mark -these records as simulated. A replacement built on things the browser genuinely can observe — real -DNS/TCP/TLS/TTFB phase timings, the CDN edge you're actually routed to, and HTTP/3 negotiation — is -the next major piece of work. +these records as simulated. Use the Edge Path Explorer above for measurements you can rely on. -### 13. 💾 History & Export +### 14. 💾 History & Export Results persist in `localStorage`. Search, filter, inspect raw JSON, and export per-tool CSVs, a master summary, or a bundled ZIP with a manifest. @@ -112,6 +125,7 @@ without touching it, so the following go directly from your browser to third par | Provider | Receives | |---|---| | `speed.cloudflare.com` | Your IP, plus tens of MB of transfer, during a speed test | +| `cdn.jsdelivr.net`, `cdnjs.cloudflare.com`, `unpkg.com` | Your IP, as Edge Path Explorer probe targets (a few KB each) | | `cloudflare-dns.com`, `dns.google` | Every domain you resolve, over encrypted DoH | | `ipwho.is`, `ipapi.co`, `freeipapi.com` | Your public IP on opening the GeoIP tool, and every IP or domain you look up | | `1.1.1.1`, `dns.quad9.net`, `doh.opendns.com`, `en.wikipedia.org` | Your IP, as latency probe targets | diff --git a/src/App.tsx b/src/App.tsx index 2af074c..e88c8cc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { getNetworkConnectionInfo } from './utils/network'; import { getHistory, getLocalStorageSizeBytes } from './utils/storage'; import { Navbar } from './components/Navbar'; import { Dashboard } from './components/Dashboard'; +import { EdgePathExplorer } from './components/EdgePathExplorer'; import { TracertVisualizer } from './components/TracertVisualizer'; import { PortScanner } from './components/PortScanner'; import { GeoIpLookup } from './components/GeoIpLookup'; @@ -79,6 +80,10 @@ export default function App() { /> )} + {activeTab === 'edgepath' && ( + + )} + {activeTab === 'tracert' && ( )} diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 7b2fa2f..00f4e97 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -359,25 +359,52 @@ export const Dashboard: React.FC = ({
{/* Traceroute TRACERT Hop Map */}
setActiveTab('tracert')} + onClick={() => setActiveTab('edgepath')} className="group bg-gradient-to-br from-cyan-950/40 via-slate-900 to-slate-900 border border-cyan-500/30 hover:border-cyan-400 rounded-2xl p-5 cursor-pointer transition-all hover:shadow-xl hover:shadow-cyan-500/10 hover:-translate-y-0.5" >
- +

- Traceroute Hop Map + Edge Path Explorer

- + New

- Trace network hops, intermediate transit nodes, GeoIP locations, and follow every hop live on an interactive map. + Real DNS, TCP, TLS and first-byte timings, the CDN edge that answered, and whether + HTTP/3 is reaching your network.

- Launch Tracert Map + Explore edge path + +
+
+ + {/* Route model (simulated) */} +
setActiveTab('tracert')} + className="group bg-slate-900 border border-slate-800 hover:border-amber-500/40 rounded-2xl p-5 cursor-pointer transition-all hover:shadow-lg hover:-translate-y-0.5" + > +
+ +
+
+

+ Route Model +

+ + Simulated + +
+

+ An illustrative great-circle path to a target. Browsers cannot traceroute, so the + intermediate hops are modelled rather than measured. +

+
+ Open route model
diff --git a/src/components/EdgeMap.tsx b/src/components/EdgeMap.tsx new file mode 100644 index 0000000..a56d83f --- /dev/null +++ b/src/components/EdgeMap.tsx @@ -0,0 +1,243 @@ +import React, { useEffect, useRef } from 'react'; +import L from 'leaflet'; + +/** + * Map of real, measured network geography. + * + * Deliberately narrower than the route map it sits beside: it draws only points + * whose coordinates came from somewhere verifiable — a CDN edge identified by + * IATA code, or a client location reported by the edge itself — plus distance + * constraints derived from round-trip time. + * + * There are no interpolated waypoints, because there is nothing to interpolate + * between: a browser cannot see the routers in the middle. + */ + +export interface MapNode { + id: string; + lat: number; + lng: number; + kind: 'client' | 'edge-pop' | 'target-pop'; + label: string; + detail: string[]; +} + +export interface MapLink { + fromId: string; + toId: string; + label?: string; +} + +/** + * A distance constraint, not a location. + * + * Radius is the furthest the client can possibly be from this point given the + * measured round trip. The client is somewhere inside the circle; the circle + * does not claim to know where. + */ +export interface MapRing { + centerLat: number; + centerLng: number; + radiusKm: number; + label: string; +} + +interface EdgeMapProps { + nodes: MapNode[]; + links: MapLink[]; + rings: MapRing[]; + heightClass?: string; +} + +const NODE_COLOUR: Record = { + client: '#10b981', // emerald + 'edge-pop': '#06b6d4', // cyan + 'target-pop': '#f43f5e', // rose +}; + +const NODE_GLYPH: Record = { + client: '◎', + 'edge-pop': '▲', + 'target-pop': '◆', +}; + +/** Element factory that applies text via textContent, never innerHTML. */ +function el(tag: string, cssText: string, text?: string): HTMLElement { + const node = document.createElement(tag); + node.style.cssText = cssText; + if (text !== undefined) node.textContent = text; + return node; +} + +function buildBadge(node: MapNode): HTMLElement { + const colour = NODE_COLOUR[node.kind]; + return el( + 'div', + `width:28px;height:28px;background:${colour};border:2px solid rgba(255,255,255,0.9);` + + 'border-radius:50%;display:flex;align-items:center;justify-content:center;color:#000;' + + `font-weight:800;font-size:13px;font-family:monospace;box-shadow:0 0 14px ${colour};`, + NODE_GLYPH[node.kind], + ); +} + +/** + * Popup content, assembled as DOM nodes. + * + * Labels here include hostnames the user typed and strings returned by remote + * services, so none of it may be interpolated into markup. + */ +function buildPopup(node: MapNode): HTMLElement { + const colour = NODE_COLOUR[node.kind]; + const wrap = el( + 'div', + 'font-family:monospace;color:#f8fafc;background:#0f172a;padding:10px;border-radius:8px;' + + 'border:1px solid #334155;min-width:180px;', + ); + wrap.appendChild( + el('div', `color:${colour};font-weight:bold;font-size:13px;margin-bottom:4px;`, node.label), + ); + for (const line of node.detail) { + wrap.appendChild(el('div', 'color:#94a3b8;font-size:11px;line-height:1.5;', line)); + } + return wrap; +} + +export const EdgeMap: React.FC = ({ + nodes, + links, + rings, + heightClass = 'h-[420px]', +}) => { + const containerRef = useRef(null); + const mapRef = useRef(null); + const layerRef = useRef(null); + + // Map instance is created once and torn down on unmount. + useEffect(() => { + if (!containerRef.current || mapRef.current) return; + + const map = L.map(containerRef.current, { + center: [20, 0], + zoom: 2, + minZoom: 1, + maxZoom: 12, + zoomControl: false, + worldCopyJump: true, + }); + + L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: + '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 19, + }).addTo(map); + + L.control.zoom({ position: 'bottomright' }).addTo(map); + layerRef.current = L.layerGroup().addTo(map); + mapRef.current = map; + + return () => { + map.remove(); + mapRef.current = null; + layerRef.current = null; + }; + }, []); + + // Redraw contents. Depends only on the data, so unrelated parent renders do + // not tear down and rebuild every layer. + useEffect(() => { + const map = mapRef.current; + const layer = layerRef.current; + if (!map || !layer) return; + + layer.clearLayers(); + + const byId = new Map(nodes.map((n) => [n.id, n])); + const bounds: [number, number][] = []; + + // Rings first so markers draw above them. + for (const ring of rings) { + L.circle([ring.centerLat, ring.centerLng], { + radius: ring.radiusKm * 1000, + color: '#06b6d4', + weight: 1, + opacity: 0.5, + fillColor: '#06b6d4', + fillOpacity: 0.05, + dashArray: '4 6', + }) + .bindTooltip(ring.label, { direction: 'top', className: 'edge-ring-tooltip' }) + .addTo(layer); + } + + for (const link of links) { + const from = byId.get(link.fromId); + const to = byId.get(link.toId); + if (!from || !to) continue; + L.polyline( + [ + [from.lat, from.lng], + [to.lat, to.lng], + ], + { color: '#06b6d4', weight: 2, opacity: 0.7, dashArray: '6 6' }, + ).addTo(layer); + } + + for (const node of nodes) { + bounds.push([node.lat, node.lng]); + L.marker([node.lat, node.lng], { + icon: L.divIcon({ + className: 'custom-tracert-marker', + html: buildBadge(node), + iconSize: [28, 28], + iconAnchor: [14, 14], + }), + title: node.label, + }) + .bindPopup(buildPopup(node), { className: 'tracert-dark-popup' }) + .addTo(layer); + } + + if (bounds.length === 1) { + map.setView(bounds[0], 5, { animate: true }); + } else if (bounds.length > 1) { + map.fitBounds(L.latLngBounds(bounds).pad(0.3), { animate: true }); + } + }, [nodes, links, rings]); + + return ( +
+
+ + {nodes.length === 0 && ( +
+

+ No mapped locations yet. Run an exploration to place your edge on the map. +

+
+ )} + +
+ {( + [ + ['client', 'You'], + ['edge-pop', 'CDN edge'], + ['target-pop', 'Target edge'], + ] as const + ).map(([kind, label]) => ( + + + ))} +
+
+ ); +}; diff --git a/src/components/EdgePathExplorer.tsx b/src/components/EdgePathExplorer.tsx new file mode 100644 index 0000000..a4c6733 --- /dev/null +++ b/src/components/EdgePathExplorer.tsx @@ -0,0 +1,514 @@ +import React, { useMemo, useState } from 'react'; +import { + Compass, + Play, + RefreshCw, + Server, + Radio, + Info, + ShieldCheck, + AlertTriangle, + CheckCircle2, +} from 'lucide-react'; +import type { EdgePathResult, EdgeProbeResult, HistoryItem, PhaseTimings } from '../types'; +import { exploreEdgePath } from '../utils/edgePath'; +import { saveHistoryItem } from '../utils/storage'; +import { ResponsibleNetworkingModal, isResponsibleNetworkingAccepted } from './ResponsibleNetworkingModal'; +import { FailureNotice, displayMetric } from './MetricValue'; +import { EdgeMap, type MapNode, type MapLink, type MapRing } from './EdgeMap'; + +interface EdgePathExplorerProps { + onHistoryUpdate: () => void; +} + +const PRESETS = ['cloudflare.com', 'discord.com', 'shopify.com', 'medium.com']; + +/** Colour and label for each phase of the connection. */ +const PHASE_SPEC = [ + { key: 'dnsMs', label: 'DNS', colour: 'bg-violet-500', hint: 'Resolving the hostname' }, + { key: 'tcpMs', label: 'TCP', colour: 'bg-cyan-500', hint: 'Opening the socket' }, + { key: 'tlsMs', label: 'TLS', colour: 'bg-emerald-500', hint: 'Negotiating encryption' }, + { key: 'ttfbMs', label: 'TTFB', colour: 'bg-amber-500', hint: 'Waiting for the first byte' }, + { key: 'transferMs', label: 'Transfer', colour: 'bg-slate-500', hint: 'Receiving the body' }, +] as const satisfies readonly { key: keyof PhaseTimings; label: string; colour: string; hint: string }[]; + +const AVAILABILITY_COPY: Record = { + available: '', + 'timing-allow-origin-missing': + 'This origin does not send Timing-Allow-Origin, so the browser zeroes its DNS, TCP and TLS timings. They are unavailable, not zero.', + 'connection-reused': + 'Answered over a connection that was already open, so there was no handshake to measure. Reload to force a fresh one.', + 'request-failed': 'The request did not complete.', +}; + +/** Stacked bar of the connection phases, proportional to their real durations. */ +const Waterfall: React.FC<{ probe: EdgeProbeResult }> = ({ probe }) => { + const segments = PHASE_SPEC.map((spec) => ({ + ...spec, + value: probe.phases[spec.key], + })).filter((s): s is (typeof PHASE_SPEC)[number] & { value: number } => s.value !== null && s.value > 0); + + const total = segments.reduce((sum, s) => sum + s.value, 0); + + if (segments.length === 0) { + return ( +
+ {AVAILABILITY_COPY[probe.availability] || 'No phase data.'} +
+ ); + } + + return ( +
+
+ {segments.map((s) => ( +
+ ))} +
+
+ {segments.map((s) => ( + + + ))} +
+
+ ); +}; + +export const EdgePathExplorer: React.FC = ({ onHistoryUpdate }) => { + const [targetHost, setTargetHost] = useState('cloudflare.com'); + const [isRunning, setIsRunning] = useState(false); + const [stage, setStage] = useState(''); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [showResponsibleModal, setShowResponsibleModal] = useState(false); + + const execute = async () => { + setIsRunning(true); + setError(null); + setResult(null); + try { + const res = await exploreEdgePath(targetHost, setStage); + setResult(res); + + const item: HistoryItem = { + id: res.id, + type: 'edgepath', + timestamp: res.timestamp, + title: `Edge path: ${res.targetHost ?? 'this connection'}${ + res.referencePop ? ` via ${res.referencePop.colo}` : '' + }`, + summary: [ + res.referencePop ? `Edge: ${res.referencePop.colo}` : 'Edge: unknown', + res.client?.asOrganization ? `ASN: ${res.client.asOrganization}` : null, + res.protocolEvidence.negotiated.length + ? `Protocol: ${res.protocolEvidence.negotiated.join(', ')}` + : null, + res.clientToPopKm !== null ? `${res.clientToPopKm} km to edge` : null, + ] + .filter(Boolean) + .join(' | '), + data: res, + }; + saveHistoryItem(item); + onHistoryUpdate(); + } catch (e) { + console.error('Edge path exploration failed:', e); + setError( + e instanceof Error ? `Exploration failed: ${e.message}` : 'Exploration failed.', + ); + } finally { + setIsRunning(false); + setStage(''); + } + }; + + const handleStart = () => { + if (!isResponsibleNetworkingAccepted()) { + setShowResponsibleModal(true); + return; + } + execute(); + }; + + // Only points with genuine coordinates reach the map. + const { nodes, links, rings } = useMemo(() => { + const n: MapNode[] = []; + const l: MapLink[] = []; + const r: MapRing[] = []; + if (!result) return { nodes: n, links: l, rings: r }; + + if (result.client?.lat != null && result.client?.lng != null) { + n.push({ + id: 'client', + lat: result.client.lat, + lng: result.client.lng, + kind: 'client', + label: 'Your connection', + detail: [ + result.client.ip ? `IP: ${result.client.ip}` : 'IP: unknown', + result.client.asOrganization + ? `Network: ${result.client.asOrganization}` + : 'Network: unknown', + result.client.asn ? `ASN: AS${result.client.asn}` : '', + [result.client.city, result.client.country].filter(Boolean).join(', '), + 'Location as reported by the edge that served you.', + ].filter(Boolean), + }); + } + + if (result.referencePop?.lat != null && result.referencePop?.lng != null) { + n.push({ + id: 'edge', + lat: result.referencePop.lat, + lng: result.referencePop.lng, + kind: 'edge-pop', + label: `Edge ${result.referencePop.colo}`, + detail: [ + [result.referencePop.city, result.referencePop.country].filter(Boolean).join(', '), + result.referencePop.httpProtocol + ? `Protocol: ${result.referencePop.httpProtocol}` + : '', + 'The CDN point of presence that answered your request.', + ].filter(Boolean), + }); + if (n.some((x) => x.id === 'client')) { + l.push({ fromId: 'client', toId: 'edge' }); + } + + // Constraint ring: the tightest round trip bounds how far the edge is. + const best = result.probes + .map((p) => p.maxDistanceKm) + .filter((d): d is number => d !== null); + if (best.length > 0) { + const radius = Math.min(...best); + r.push({ + centerLat: result.referencePop.lat, + centerLng: result.referencePop.lng, + radiusKm: radius, + label: `Within ${radius.toLocaleString()} km of this edge (speed-of-light limit)`, + }); + } + } + + if (result.targetPop?.lat != null && result.targetPop?.lng != null) { + n.push({ + id: 'target', + lat: result.targetPop.lat, + lng: result.targetPop.lng, + kind: 'target-pop', + label: `${result.targetHost ?? 'Target'} → ${result.targetPop.colo}`, + detail: [ + [result.targetPop.city, result.targetPop.country].filter(Boolean).join(', '), + result.targetPop.httpProtocol ? `Protocol: ${result.targetPop.httpProtocol}` : '', + 'The edge this host reports serving your traffic from.', + ].filter(Boolean), + }); + if (n.some((x) => x.id === 'client')) { + l.push({ fromId: 'client', toId: 'target' }); + } + } + + return { nodes: n, links: l, rings: r }; + }, [result]); + + const verdictStyle = (() => { + switch (result?.protocolEvidence.verdict) { + case 'http3-working': + return { icon: CheckCircle2, cls: 'text-emerald-300 bg-emerald-500/10 border-emerald-500/30' }; + case 'http3-absent-udp-possibly-blocked': + return { icon: AlertTriangle, cls: 'text-amber-300 bg-amber-500/10 border-amber-500/30' }; + case 'legacy-http1': + return { icon: AlertTriangle, cls: 'text-rose-300 bg-rose-500/10 border-rose-500/30' }; + default: + return { icon: Info, cls: 'text-slate-400 bg-slate-800/50 border-slate-700/50' }; + } + })(); + const VerdictIcon = verdictStyle.icon; + + return ( +
+ {/* Header */} +
+
+
+ +

Edge Path Explorer

+ + Measured + +
+

+ Everything a browser can genuinely observe about the path to a host: the real + DNS → TCP → TLS → first-byte breakdown, which CDN edge answered, the protocol that was + negotiated, and how far away a server can possibly be. +

+
+ + +
+ + {/* Why there are no intermediate hops. */} +
+ +

+ Why there are no router-by-router hops.{' '} + A traceroute works by sending packets with a deliberately small IP TTL and reading the + errors that come back. A web page can do neither — there are no raw sockets and no TTL + control in the browser. So rather than invent the middle of the path, this tool measures + the endpoints precisely: the phases of a real connection, the edge that terminated it, and + a distance bound that physics guarantees. +

+
+ + {isRunning && stage && ( +

+ {stage}… +

+ )} + + {error && ( +
+ {error} +
+ )} + + {/* Target */} +
+ + setTargetHost(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && !isRunning && handleStart()} + placeholder="e.g. cloudflare.com" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-3 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500 shadow-inner" + /> +
+ Presets: + {PRESETS.map((p) => ( + + ))} +
+

+ Only Cloudflare-fronted hosts expose their edge location to a browser. For anything else + the field is reported as unavailable rather than guessed — the rest of the exploration + still runs. +

+
+ + {result && ( + <> + + + {/* Summary tiles */} +
+ {[ + { + label: 'Serving edge', + value: result.referencePop?.colo ?? '—', + sub: result.referencePop + ? [result.referencePop.city, result.referencePop.country].filter(Boolean).join(', ') || + 'Location not in the bundled table' + : 'Not determined', + icon: Server, + }, + { + label: 'Distance to edge', + value: result.clientToPopKm !== null ? `${result.clientToPopKm.toLocaleString()}` : '—', + sub: result.clientToPopKm !== null ? 'km, great-circle' : 'Needs both locations', + icon: Compass, + }, + { + label: 'Your network', + value: result.client?.asn ? `AS${result.client.asn}` : '—', + sub: result.client?.asOrganization ?? 'Not determined', + icon: Radio, + }, + { + label: 'Protocol', + value: result.protocolEvidence.negotiated[0] ?? '—', + sub: result.protocolEvidence.negotiated.length > 1 + ? `also ${result.protocolEvidence.negotiated.slice(1).join(', ')}` + : 'Across probed origins', + icon: ShieldCheck, + }, + ].map((tile) => ( +
+
+ + {tile.label} +
+
{tile.value}
+
{tile.sub}
+
+ ))} +
+ + {/* The answer to what the user actually asked. Without this the target + host's edge was only reachable by opening a map popup. */} + {result.targetHost && ( +
+

+ {result.targetHost} +

+ {result.targetPop ? ( +
+ Your traffic enters at + + {result.targetPop.colo} + + + {[result.targetPop.city, result.targetPop.country].filter(Boolean).join(', ') || + 'an edge not present in the bundled location table'} + + {result.targetPop.httpProtocol && ( + + {result.targetPop.httpProtocol} + + )} +
+ ) : ( +

+ This host did not report an edge location. Only Cloudflare-fronted sites expose{' '} + /cdn-cgi/trace to a browser, so this is the + normal result for most of the web — not a failure of your network. +

+ )} +
+ )} + + {/* Map */} +
+
+

+ Measured geography +

+

+ Dashed circle = furthest the edge can be, given round-trip time +

+
+ +
+ + {/* Protocol verdict */} +
+ +
+

+ {result.protocolEvidence.verdict === 'http3-working' && 'HTTP/3 is working'} + {result.protocolEvidence.verdict === 'http3-absent-udp-possibly-blocked' && + 'HTTP/3 unavailable — UDP/443 may be blocked'} + {result.protocolEvidence.verdict === 'legacy-http1' && 'Connections fell back to HTTP/1.1'} + {result.protocolEvidence.verdict === 'http2-only' && 'HTTP/2 in use'} + {result.protocolEvidence.verdict === null && 'Protocol not observable'} +

+

+ {result.protocolEvidence.explanation} +

+
+
+ + {/* Waterfalls */} +
+
+

+ Connection phase breakdown +

+

+ Real timings from the Performance Timeline. Handshake phases only exist on a + connection’s first request. +

+
+ + {result.probes.map((probe) => ( +
+
+
+ {probe.target.label} + {probe.target.origin} + {probe.protocol && ( + + {probe.protocol} + + )} +
+
+ + RTT {displayMetric(probe.roundTripMs, 'ms', 1)} + + {probe.maxDistanceKm !== null && ( + + ≤ {probe.maxDistanceKm.toLocaleString()} km away + + )} +
+
+ + {probe.availability !== 'available' && ( +

+ {probe.error ?? AVAILABILITY_COPY[probe.availability]} +

+ )} +
+ ))} +
+ + )} + + setShowResponsibleModal(false)} + onConfirm={() => { + setShowResponsibleModal(false); + execute(); + }} + /> +
+ ); +}; diff --git a/src/components/HistoryLog.tsx b/src/components/HistoryLog.tsx index 41d7ada..ba98a96 100644 --- a/src/components/HistoryLog.tsx +++ b/src/components/HistoryLog.tsx @@ -124,7 +124,7 @@ export const HistoryLog: React.FC = ({ history, onHistoryUpdate {/* Type Filter Buttons */}
- {['all', 'tracert', 'portscanner', 'speedtest', 'ping', 'dns', 'webrtc', 'cidr', 'mac', 'httpprobe', 'websocket'].map((t) => ( + {['all', 'edgepath', 'tracert', 'portscanner', 'geoip', 'speedtest', 'ping', 'dns', 'webrtc', 'cidr', 'mac', 'httpprobe', 'websocket'].map((t) => (