fix(wifi): make Connect work from the setup AP (#571)

* fix(wifi): make Connect work from the setup AP

Joining a network from LEDMatrix-Setup has to take the AP down first, which
drops the phone that sent the request. The connect endpoint answered only
after the attempt finished, so the browser never got a reply and the WiFi
tab's Connect button appeared to do nothing.

- /wifi/connect answers 202 immediately while the AP is active and connects
  in a background thread; the result (never the password) is reported via
  /wifi/status as last_connect_attempt. A second connect while one is
  pending gets 409.
- connect_to_network holds a /tmp flag for the attempt; the monitor daemon
  skips AP management while it is fresh. Previously the daemon's
  disconnected counter, accumulated over the whole AP session, re-enabled
  the AP on its next tick in the middle of the connect.
- The WiFi tab and captive setup page explain the handoff up front, and on
  reopening show why the last attempt failed. The wrong-password message
  now works: the route sets the error_type the captive page checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(wifi): serialize connect attempts on both paths

Addresses CodeRabbit review on #571:

- Check for a pending attempt before branching on AP state. A background
  attempt takes the AP down long before it finishes, so a second click
  used to bypass the 409 and start a competing synchronous connect.
- Record pending for the synchronous (non-AP) path too, so two requests
  can't overlap and have the first clear the daemon's in-progress flag
  while the second is still connecting.
- Clear the pending state if the background thread fails to start, rather
  than refusing every later request until restart.
- Say the setup network returns "within a few minutes": a stale flag plus
  the daemon's grace period can take longer than one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-14 12:40:41 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent d51f7ada14
commit 7e580dc005
7 changed files with 484 additions and 31 deletions
+73 -4
View File
@@ -197,7 +197,11 @@
<!-- Messages -->
<div x-show="message"
x-text="message"
:class="messageType === 'error' ? 'bg-red-50 border-red-200 text-red-800' : 'bg-green-50 border-green-200 text-green-800'"
:class="{
error: 'bg-red-50 border-red-200 text-red-800',
info: 'bg-blue-50 border-blue-200 text-blue-800',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800'
}[messageType] || 'bg-green-50 border-green-200 text-green-800'"
class="mt-4 p-3 rounded border"
x-cloak></div>
</div>
@@ -214,6 +218,7 @@ function wifiSetup() {
ap_mode_active: false,
auto_enable_ap_mode: true // Default: true (safe due to grace period)
},
statusLoaded: false,
networks: [],
selectedSSID: '',
manualSSID: '',
@@ -258,7 +263,15 @@ function wifiSetup() {
const response = await fetch('/api/v3/wifi/status');
const data = await response.json();
if (data.status === 'success') {
const firstLoad = !this.statusLoaded;
this.status = data.data;
this.statusLoaded = true;
// A failed attempt brings LEDMatrix-Setup back; tell the
// user who rejoined it why.
const attempt = data.data.last_connect_attempt;
if (firstLoad && attempt && attempt.state === 'failed' && !data.data.connected) {
this.showMessage(this.describeFailedAttempt(attempt), 'error');
}
} else {
this.showMessage(data.message, 'error');
}
@@ -316,6 +329,10 @@ function wifiSetup() {
this.connecting = true;
this.message = '';
const viaSetupNetwork = this.status.ap_mode_active;
if (viaSetupNetwork) {
this.showMessage(this.handoffMessage(ssid.trim()), 'info');
}
try {
const response = await fetch('/api/v3/wifi/connect', {
method: 'POST',
@@ -327,7 +344,16 @@ function wifiSetup() {
password: this.password || ''
})
});
if (response.status === 202) {
// Connecting in the background; the setup network is about
// to go down and take this page's connection with it.
this.password = '';
this.showMessage(this.handoffMessage(ssid.trim()), 'info');
this.pollConnectResult(ssid.trim());
return;
}
// Check if response is ok before parsing JSON
if (!response.ok) {
// Try to parse error message from response
@@ -362,13 +388,56 @@ function wifiSetup() {
this.showMessage(data.message || 'Failed to connect to network', 'error');
}
} catch (error) {
console.error('WiFi connect error:', error);
this.showMessage('Failed to connect: ' + error.message, 'error');
if (viaSetupNetwork) {
// The setup network went down mid-request: that is the
// connection attempt happening, not a failure.
this.showMessage(this.handoffMessage(ssid.trim()), 'info');
} else {
console.error('WiFi connect error:', error);
this.showMessage('Failed to connect: ' + error.message, 'error');
}
} finally {
this.connecting = false;
}
},
handoffMessage(ssid) {
return `Connecting to ${ssid}. LEDMatrix-Setup will turn off, so this page will stop responding. ` +
`If it works, the LED display shows the new address; rejoin your home WiFi and open it. ` +
`If it fails, LEDMatrix-Setup comes back within a few minutes: rejoin it and reopen this page to see why.`;
},
// Follow a background connect until its result lands or the setup
// network drops out from under this page.
async pollConnectResult(ssid, retriesLeft = 20) {
if (retriesLeft <= 0) return;
await new Promise(resolve => setTimeout(resolve, 3000));
try {
const response = await fetch('/api/v3/wifi/status');
const data = await response.json();
const attempt = data.data && data.data.last_connect_attempt;
if (attempt && attempt.ssid === ssid && attempt.state !== 'pending') {
this.status = data.data;
if (attempt.state === 'success') {
this.showMessage(attempt.message || `Connected to ${ssid}`, 'success');
} else {
this.showMessage(this.describeFailedAttempt(attempt), 'error');
}
return;
}
} catch (error) {
return; // Setup network is down; the handoff message already explains.
}
this.pollConnectResult(ssid, retriesLeft - 1);
},
describeFailedAttempt(attempt) {
const reason = attempt.error_type === 'wrong_password'
? 'the password was incorrect'
: (attempt.message || 'unknown error');
return `Could not connect to ${attempt.ssid}: ${reason}.`;
},
async disconnectFromNetwork() {
if (!this.status.connected) {
this.showMessage('Not connected to any network', 'warning');