mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
feat(web): confidence tier for network-timeline hotspot badge
An SSID matching the typical iPhone hotspot name (".. iPhone") is strong
evidence on its own — such networks now get a plain "Hotspot" badge, while
subnet-only evidence keeps the hedged "Hotspot?". Tooltip and footnote
explain the distinction; detection also fires on iPhone-named SSIDs
outside the Apple hotspot subnet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 && (
|
||||
<span className="text-[10px] px-1 py-px rounded bg-amber-100 text-amber-800 font-medium">Hotspot?</span>
|
||||
<span className="text-[10px] px-1 py-px rounded bg-amber-100 text-amber-800 font-medium">
|
||||
{l.hotspot.confident ? 'Hotspot' : 'Hotspot?'}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
@@ -191,7 +196,7 @@ export default function NetworkBand({ model }: { model: NetworkModel }) {
|
||||
{seg.adapterDescription && <div>{seg.adapterDescription}</div>}
|
||||
{seg.hotspot && (
|
||||
<div className="text-amber-300">
|
||||
Probable smartphone hotspot ({seg.hotspot.vendor}) — {seg.hotspot.reason}
|
||||
{`${seg.hotspot.confident ? 'Smartphone hotspot' : 'Probable smartphone hotspot'} (${seg.hotspot.vendor}) — ${seg.hotspot.reason}`}
|
||||
</div>
|
||||
)}
|
||||
{seg.ssidInferred && <div className="text-amber-300">SSID inferred from matching gateway</div>}
|
||||
|
||||
+23
-3
@@ -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 "<name>'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;
|
||||
}
|
||||
|
||||
@@ -136,8 +136,8 @@ function Content() {
|
||||
<NetworkBand model={model} />
|
||||
{model.hotspotDetected && (
|
||||
<p className="mt-1 text-[11px] text-amber-700">
|
||||
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.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user