fix(phase26): skip netwatch AP scan when clients connected (BUG-01)

This commit is contained in:
Mathieu Moran
2026-08-11 03:11:13 -04:00
parent f9d967e71c
commit 89bbc4fdb8
3 changed files with 71 additions and 0 deletions
+19
View File
@@ -220,9 +220,28 @@ def restore_fallback():
return False return False
def _ap_client_count():
"""Return number of stations connected to the AP on INTERFACE."""
try:
proc = subprocess.run(
["iw", "dev", INTERFACE, "station", "dump"],
capture_output=True, text=True, timeout=5,
)
return proc.stdout.count("Station ")
except (OSError, subprocess.SubprocessError):
return 0
def check_uplink(): def check_uplink():
current = active_connection() current = active_connection()
phone_configured = connection_exists(PHONE_CONNECTION) phone_configured = connection_exists(PHONE_CONNECTION)
# Never tear down the AP if a client is connected — the user is actively
# using the hotspot (e.g. phone browsing the web UI while driving).
# Skip this netwatch cycle and let the next one try again.
if current == AP_CONNECTION and _ap_client_count() > 0:
write_status("ap_has_clients_skipped_check")
return True
if current == PHONE_CONNECTION: if current == PHONE_CONNECTION:
if internet_reachable(): if internet_reachable():
write_status("phone_connected") write_status("phone_connected")
+23
View File
@@ -248,6 +248,29 @@ async function loadUplinkStatus() {
const status = await response.json(); const status = await response.json();
if (!response.ok) throw new Error(status.error || `HTTP ${response.status}`); if (!response.ok) throw new Error(status.error || `HTTP ${response.status}`);
uplinkStatus.textContent = describeUplink(status); uplinkStatus.textContent = describeUplink(status);
// In-car access card
const hotspotInfo = document.getElementById('in-car-hotspot-info');
const wifiInfo = document.getElementById('in-car-wifi-info');
const disconnectedInfo = document.getElementById('in-car-disconnected-info');
const hotspotUrl = document.getElementById('hotspot-url');
const copyBtn = document.getElementById('copy-hotspot-url');
if (status.mode === 'local_hotspot') {
const ip = status.ip_address || '10.42.0.1';
hotspotUrl.textContent = `http://${ip}`;
hotspotInfo.hidden = false;
wifiInfo.hidden = true;
disconnectedInfo.hidden = true;
if (copyBtn) copyBtn.onclick = () => navigator.clipboard.writeText(`http://${ip}`);
} else if (status.mode === 'wifi' || status.mode === 'phone') {
hotspotInfo.hidden = true;
wifiInfo.hidden = false;
disconnectedInfo.hidden = true;
} else {
hotspotInfo.hidden = true;
wifiInfo.hidden = true;
disconnectedInfo.hidden = false;
}
const phoneAddress = status.mode === 'phone' const phoneAddress = status.mode === 'phone'
? status.ip_address ? status.ip_address
: status.last_transition && status.last_transition.last_phone_ip; : status.last_transition && status.last_transition.last_phone_ip;
+29
View File
@@ -134,6 +134,35 @@
</div> </div>
</div> </div>
{# ── In-car access ────────────────────────────────────────────────── #}
<div class="card shadow-sm mb-3" id="in-car-access-card">
<div class="card-header py-2">
<p class="text-primary m-0 fw-bold small">In-car access</p>
</div>
<div class="card-body py-3">
<div id="in-car-hotspot-info" hidden>
<p class="mb-2 fw-semibold">Your phone can reach the Pi at:</p>
<div class="d-flex align-items-center gap-2 mb-3">
<code class="fs-5" id="hotspot-url">http://10.42.0.1</code>
<button class="btn btn-outline-secondary btn-sm" id="copy-hotspot-url" type="button" title="Copy">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="alert alert-warning py-2 mb-0 small">
<i class="fas fa-exclamation-triangle me-1"></i>
If the page doesn't load, try <strong>disabling cellular/mobile data</strong> on your phone.
Phones often keep using cell data when WiFi has no internet, blocking access to the Pi.
</div>
</div>
<div id="in-car-wifi-info" hidden>
<p class="mb-1 small text-muted">The Pi is connected to WiFi. Open <code>http://yaris.local</code> from any device on the same network.</p>
</div>
<div id="in-car-disconnected-info" hidden>
<p class="mb-0 small text-muted">Waiting for network…</p>
</div>
</div>
</div>
{# ── Phone hotspot ───────────────────────────────────────────────── #} {# ── Phone hotspot ───────────────────────────────────────────────── #}
<div class="card shadow-sm mb-3"> <div class="card shadow-sm mb-3">
<div class="card-header py-2"> <div class="card-header py-2">