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.
)}