Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,40 @@ by design and would manufacture findings out of geography.

## 🛠️ Tools

### 🚶 Walk & Test — *survey the building, not just the connection*

Ten destinations people actually depend on — Google, YouTube, Netflix, Facebook and Amazon on the
consumer side; Microsoft 365, Teams, Zoom, Salesforce and Slack on the business side — probed round
after round while you walk the building. Name the spot you are standing in, wait for a few rounds,
move, name the next one. The result is a **per-spot comparison**: which corner of the office loses
Teams, where the round trips double, which dead spot is a dead spot for everything and which is
only bad for one destination.

The repeated-probe idea is [Richard Astbury's Azure Speed Test](https://richorama.github.io/AzureSpeedTest2/),
which times a fixed list of regions until the table stops moving. This one expects you to move
instead.

What the numbers are, stated permanently on screen rather than in a footnote:

- **Each figure is a full HTTPS request round trip, not a ping.** No raw sockets means no ICMP and
no lower-level timing. TLS on a new connection and the destination's own front-end are inside
every number.
- **The requests are `no-cors`, so the response is opaque.** A completed probe proves the edge
answered and how long it took. A 200, a 403 and a login redirect are indistinguishable from here,
and the tool claims nothing about which it got.
- **These are front doors, not backends.** Netflix playback, Teams call audio and Zoom media run
over paths a browser cannot address, so a green row does not promise a smooth call.
- **Unanswered is not "down", and it is not packet loss.** A timeout, a refused connection, a failed
lookup and being out of range look identical to a browser. The column is called *Answered*.
- **All ten fire at once each round**, which gives them the same instant — and makes them compete
on a constrained link. Compare rows and spots to each other, not a single figure to a spec sheet.

Each destination's first probe pays for DNS, TCP and TLS, so it is counted but kept out of the
timing statistics. Spots are compared by the **median of each destination's own median**, over only
the destinations that produced a median at *every* spot — pooling raw samples instead would make the
figure lurch when a destination dropped out, reporting a change in which destinations answered as
though it were a change in latency.

### 1. ⚡ Speed & Bandwidth
Streams real data over three concurrent connections against the Cloudflare edge, measuring
throughput from bytes that actually moved. Reports download, upload, idle latency, jitter, latency
Expand Down Expand Up @@ -234,6 +268,7 @@ without touching it, so the following go directly from your browser to third par
| `1.1.1.1`, `one.one.one.one`, `dns.quad9.net`, `doh.opendns.com`, `en.wikipedia.org` | Your IP, as latency probe targets, and as the two halves of the resolver test |
| `ipv4.icanhazip.com`, `ipv6.icanhazip.com`, `api4.ipify.org`, `api6.ipify.org` | Your IP, during the dual-stack check — each answers on one address family only |
| `cp.cloudflare.com` | Your IP, during the captive-portal check, and only when NetReady is opened over plain `http` |
| `www.google.com`, `www.youtube.com`, `www.netflix.com`, `www.facebook.com`, `www.amazon.com`, `outlook.office365.com`, `teams.microsoft.com`, `zoom.us`, `login.salesforce.com`, `slack.com` | Your IP, once per round for the length of a Walk & Test run — roughly 200 requests each over ten minutes. One HEAD for a small public file, no cookies sent |
| `stun.l.google.com` and other STUN servers | Your public IP, and potentially local addresses |
| `httpbin.org` | Your IP, only when you press "Trigger Network Spike" on the live traffic monitor |
| `basemaps.cartocdn.com`, `openstreetmap.org` | Map areas you view, revealing an approximate target location |
Expand Down Expand Up @@ -267,6 +302,10 @@ properly — over UDP, against their actual IP addresses — since 2010. The cac
"dotcom" separation, the NXDOMAIN-redirection check and the plain-English conclusions are all his
design; NetReady reproduces what a browser honestly can and says plainly where it cannot follow.

Walk & Test borrows its shape from **Richard Astbury's**
[Azure Speed Test](https://richorama.github.io/AzureSpeedTest2/), which probes a fixed list of
destinations over and over and lets the table settle rather than reporting one number and stopping.

[Lucide](https://lucide.dev/) · [Tailwind CSS](https://tailwindcss.com/) ·
[Vite](https://vitejs.dev/) · [React](https://react.dev/) · [Leaflet](https://leafletjs.com/) ·
[Recharts](https://recharts.org/) · [Cloudflare](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/)
Expand Down
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { getHistory, getLocalStorageSizeBytes } from './utils/storage';
import { Navbar } from './components/Navbar';
import { Dashboard } from './components/Dashboard';
import { WalkTest } from './components/WalkTest';
import { TriagePanel } from './components/TriagePanel';
import { DualStackCheck } from './components/DualStackCheck';
import { DnsBenchmark } from './components/DnsBenchmark';
Expand Down Expand Up @@ -45,7 +46,7 @@
window.addEventListener('online', handleStatusChange);
window.addEventListener('offline', handleStatusChange);

const connection = (navigator as any).connection;

Check warning on line 49 in src/App.tsx

View workflow job for this annotation

GitHub Actions / check

Unexpected any. Specify a different type
if (connection) {
connection.addEventListener('change', handleStatusChange);
}
Expand Down Expand Up @@ -84,6 +85,8 @@
/>
)}

{activeTab === 'walktest' && <WalkTest onHistoryUpdate={refreshHistory} />}

{activeTab === 'triage' && <TriagePanel onHistoryUpdate={refreshHistory} />}

{activeTab === 'dualstack' && <DualStackCheck onHistoryUpdate={refreshHistory} />}
Expand Down
29 changes: 29 additions & 0 deletions src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
Network,
ShieldQuestion,
Timer,
Footprints,
MapPin,
} from 'lucide-react';
import { ToolTab, NetworkConnectionInfo, SpeedTestResult, PingResult, HistoryItem } from '../types';
import {
Expand Down Expand Up @@ -377,6 +379,33 @@ export const Dashboard: React.FC<DashboardProps> = ({
</h2>

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Walk & Test — the only tool here that expects you to move */}
<div
onClick={() => setActiveTab('walktest')}
className="group bg-gradient-to-br from-teal-950/50 via-slate-900 to-slate-900 border border-teal-500/40 hover:border-teal-400 rounded-2xl p-5 cursor-pointer transition-all hover:shadow-xl hover:shadow-teal-500/10 hover:-translate-y-0.5"
>
<div className="w-10 h-10 rounded-xl bg-teal-500 text-black flex items-center justify-center mb-3 shadow-[0_0_15px_rgba(20,184,166,0.4)]">
<Footprints className="w-5 h-5 stroke-[2.5]" />
</div>
<div className="flex items-center space-x-2 mb-1">
<h3 className="text-base font-bold text-white group-hover:text-teal-300 transition-colors">
Walk &amp; Test
</h3>
<span className="px-1.5 py-0.5 text-[9px] font-mono font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 rounded uppercase">
New
</span>
</div>
<p className="text-xs text-slate-300 line-clamp-2">
Ten consumer and business destinations probed round after round while you walk the
building. Name each spot as you reach it and compare them side by side.
</p>
<div className="mt-4 flex items-center text-xs font-semibold text-teal-300 group-hover:translate-x-1 transition-transform">
<MapPin className="w-3.5 h-3.5 mr-1" />
<span>Survey the building</span>
<ArrowRight className="w-3.5 h-3.5 ml-1" />
</div>
</div>

{/* Triage — the answer layer */}
<div
onClick={() => setActiveTab('triage')}
Expand Down
3 changes: 3 additions & 0 deletions src/components/ExportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
Network,
ShieldQuestion,
Timer,
Footprints,
} from 'lucide-react';
import { HistoryItem } from '../types';
import {
Expand Down Expand Up @@ -95,6 +96,8 @@
return <Network className={`${className} text-indigo-400`} />;
case 'captive':
return <ShieldQuestion className={`${className} text-amber-400`} />;
case 'walktest':
return <Footprints className={`${className} text-teal-400`} />;
default:
return <FileSpreadsheet className={`${className} text-slate-400`} />;
}
Expand Down Expand Up @@ -329,7 +332,7 @@
</label>
<select
value={dateFilter}
onChange={(e) => setDateFilter(e.target.value as any)}

Check warning on line 335 in src/components/ExportPage.tsx

View workflow job for this annotation

GitHub Actions / check

Unexpected any. Specify a different type
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-200 focus:outline-none focus:border-cyan-500 font-mono"
>
<option value="all">All Time History</option>
Expand Down
2 changes: 2 additions & 0 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
Network,
ShieldQuestion,
Timer,
Footprints,
} from 'lucide-react';
import { ToolTab, NetworkConnectionInfo } from '../types';
import { PrivacySafetyModal } from './PrivacySafetyModal';
Expand Down Expand Up @@ -55,6 +56,7 @@ export const Navbar: React.FC<NavbarProps> = ({

const tabs: { id: ToolTab; label: string; icon: React.FC<{ className?: string }>; badge?: string }[] = [
{ id: 'dashboard', label: 'Dashboard', icon: Activity },
{ id: 'walktest', label: 'Walk & Test', icon: Footprints, badge: 'NEW' },
{ id: 'triage', label: 'Me or the Internet?', icon: Stethoscope, badge: 'NEW' },
{ id: 'dualstack', label: 'IPv4 / IPv6', icon: Network, badge: 'NEW' },
{ id: 'captive', label: 'Portal & DNS Hijack', icon: ShieldQuestion, badge: 'NEW' },
Expand Down
12 changes: 12 additions & 0 deletions src/components/PrivacySafetyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ export const THIRD_PARTY_DISCLOSURES: { host: string; receives: string }[] = [
'Your IP, during the captive-portal check — but only when NetReady is opened over plain ' +
'http. A page served over https cannot make this request at all.',
},
{
host:
'www.google.com, www.youtube.com, www.netflix.com, www.facebook.com, www.amazon.com, ' +
'outlook.office365.com, teams.microsoft.com, zoom.us, login.salesforce.com, slack.com',
receives:
'Your IP, repeatedly, for as long as a Walk & Test run lasts — every one of them is probed ' +
'once per round, so a ten-minute walk at the default interval is roughly two hundred ' +
'requests to each. Each request is a HEAD for one small public file and carries no cookies ' +
'(`credentials: \'omit\'`), so these hosts see an address and a TLS handshake rather than a ' +
'logged-in user. Several of them are advertising businesses; the IP and the timing pattern ' +
'are still theirs to log.',
},
{
host: 'stun.l.google.com (and other STUN servers)',
receives: 'Your public IP, and potentially local network addresses, during WebRTC analysis.',
Expand Down
Loading
Loading