diff --git a/src/Web/autopilot-monitor-web/app/sessions/network-timeline/NetworkBand.tsx b/src/Web/autopilot-monitor-web/app/sessions/network-timeline/NetworkBand.tsx index 24d09046..3927e45a 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/network-timeline/NetworkBand.tsx +++ b/src/Web/autopilot-monitor-web/app/sessions/network-timeline/NetworkBand.tsx @@ -12,6 +12,7 @@ import { useCallback, useRef, useState } from 'react'; import { fmtDuration, fmtTime, + HotspotHint, NetworkModel, OFFLINE_COLOR, segmentTitle, @@ -60,12 +61,14 @@ export default function NetworkBand({ model }: { model: NetworkModel }) { } }); - // Legend entries in first-seen order - const legend: { identity: string; label: string; color: string; kind: SegmentKind; hotspot?: boolean }[] = []; + // Legend entries in first-seen order; the strongest hotspot hint wins. + const legend: { identity: string; label: string; color: string; kind: SegmentKind; hotspot?: HotspotHint }[] = []; for (const seg of segments) { const existing = legend.find((l) => l.identity === seg.identity); if (existing) { - existing.hotspot = existing.hotspot || !!seg.hotspot; + if (seg.hotspot && (!existing.hotspot || (seg.hotspot.confident && !existing.hotspot.confident))) { + existing.hotspot = seg.hotspot; + } continue; } legend.push({ @@ -73,7 +76,7 @@ export default function NetworkBand({ model }: { model: NetworkModel }) { label: segmentTitle(seg), color: colors.get(seg.identity) ?? UNKNOWN_COLOR, kind: seg.kind, - hotspot: !!seg.hotspot, + hotspot: seg.hotspot, }); } @@ -93,7 +96,9 @@ export default function NetworkBand({ model }: { model: NetworkModel }) { )} {l.label} {l.hotspot && ( - Hotspot? + + {l.hotspot.confident ? 'Hotspot' : 'Hotspot?'} + )} ))} @@ -191,7 +196,7 @@ export default function NetworkBand({ model }: { model: NetworkModel }) { {seg.adapterDescription &&
{seg.adapterDescription}
} {seg.hotspot && (
- Probable smartphone hotspot ({seg.hotspot.vendor}) — {seg.hotspot.reason} + {`${seg.hotspot.confident ? 'Smartphone hotspot' : 'Probable smartphone hotspot'} (${seg.hotspot.vendor}) — ${seg.hotspot.reason}`}
)} {seg.ssidInferred &&
SSID inferred from matching gateway
} diff --git a/src/Web/autopilot-monitor-web/app/sessions/network-timeline/networkTimelineModel.ts b/src/Web/autopilot-monitor-web/app/sessions/network-timeline/networkTimelineModel.ts index c6d2aac2..fb08c6dd 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/network-timeline/networkTimelineModel.ts +++ b/src/Web/autopilot-monitor-web/app/sessions/network-timeline/networkTimelineModel.ts @@ -23,6 +23,12 @@ export type SegmentKind = 'ethernet' | 'wifi' | 'offline' | 'unknown'; export interface HotspotHint { vendor: 'Apple' | 'Windows' | 'Android'; reason: string; + /** + * true when the SSID itself matches a typical phone-hotspot name + * (e.g. "xyz's iPhone") — strong enough to drop the "?" from the badge. + * false = subnet-only evidence, shown as "Hotspot?". + */ + confident: boolean; } export interface NetSegment { @@ -141,14 +147,28 @@ export function detectHotspot(seg: NetSegment): HotspotHint | undefined { if (seg.kind !== 'wifi') return undefined; const gw = seg.gateway ?? ''; const ip = seg.ip ?? ''; + // iOS default hotspot SSID is "'s iPhone" (localized variants keep + // "iPhone"). A user COULD name any AP like this, but that is atypical — + // treat an iPhone-named SSID as confident, not just probable. + const iphoneSsid = /iphone/i.test(seg.ssid ?? ''); + if (gw.includes('172.20.10.1') || ip.startsWith('172.20.10.')) { - return { vendor: 'Apple', reason: 'subnet 172.20.10.x — hard-wired by iOS for the personal hotspot' }; + return { + vendor: 'Apple', + confident: iphoneSsid, + reason: iphoneSsid + ? 'subnet 172.20.10.x (hard-wired by iOS) and the SSID matches the typical iPhone hotspot name' + : 'subnet 172.20.10.x — hard-wired by iOS for the personal hotspot', + }; } if (gw.includes('192.168.137.1') || ip.startsWith('192.168.137.')) { - return { vendor: 'Windows', reason: 'subnet 192.168.137.x — Windows mobile hotspot default' }; + return { vendor: 'Windows', confident: false, reason: 'subnet 192.168.137.x — Windows mobile hotspot default' }; } if (gw.includes('192.168.43.1') || ip.startsWith('192.168.43.')) { - return { vendor: 'Android', reason: 'subnet 192.168.43.x — classic Android hotspot default' }; + return { vendor: 'Android', confident: false, reason: 'subnet 192.168.43.x — classic Android hotspot default' }; + } + if (iphoneSsid) { + return { vendor: 'Apple', confident: true, reason: `SSID "${seg.ssid}" matches the typical iPhone hotspot name` }; } return undefined; } diff --git a/src/Web/autopilot-monitor-web/app/sessions/network-timeline/page.tsx b/src/Web/autopilot-monitor-web/app/sessions/network-timeline/page.tsx index 1e4d8bf0..d5a2f23e 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/network-timeline/page.tsx +++ b/src/Web/autopilot-monitor-web/app/sessions/network-timeline/page.tsx @@ -136,8 +136,8 @@ function Content() { {model.hotspotDetected && (

- Hotspot classification is a heuristic based on vendor-default subnets (e.g. 172.20.10.x on iOS) — - not a proven fact. + Hotspot classification is heuristic: "Hotspot?" marks subnet-only evidence (e.g. 172.20.10.x + on iOS); without the "?" the SSID itself matches a typical phone hotspot name.

)}