mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
* 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>
575 lines
26 KiB
HTML
575 lines
26 KiB
HTML
{% import 'v3/partials/_macros.html' as ui %}
|
|
<div class="bg-white rounded-lg shadow p-6" x-data="wifiSetup()" x-init="init(); loadStatus()">
|
|
<!-- Captive Portal Banner (shown when AP mode is active) -->
|
|
<div x-show="status.ap_mode_active"
|
|
class="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg"
|
|
x-cloak>
|
|
<div class="flex items-start">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-info-circle text-blue-600 text-lg"></i>
|
|
</div>
|
|
<div class="ml-3 flex-1">
|
|
<h3 class="text-sm font-medium text-blue-800">Captive Portal Active</h3>
|
|
<p class="mt-1 text-sm text-blue-700">
|
|
You're connected to the LEDMatrix-Setup network. Configure your WiFi connection below to connect to your home network.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="border-b border-gray-200 pb-4 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-900">
|
|
<i class="fas fa-wifi mr-2"></i>WiFi Setup
|
|
</h2>
|
|
<p class="mt-1 text-sm text-gray-600">Configure WiFi connection for your Raspberry Pi. Access point mode will automatically activate when no WiFi connection is detected.</p>
|
|
</div>
|
|
|
|
{{ ui.settings_filter('Filter WiFi settings…') }}
|
|
|
|
<!-- Current WiFi Status -->
|
|
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
|
<h3 class="text-sm font-medium text-gray-900 mb-2">Current Status</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<span class="text-sm text-gray-600">Connection:</span>
|
|
<span x-text="status.connected ? 'Connected' : 'Not Connected'"
|
|
:class="status.connected ? 'text-green-600 font-medium' : 'text-red-600 font-medium'"
|
|
class="ml-2"></span>
|
|
</div>
|
|
<div x-show="status.connected">
|
|
<span class="text-sm text-gray-600">Network:</span>
|
|
<span class="ml-2 font-medium" x-text="status.ssid || 'Unknown'"></span>
|
|
</div>
|
|
<div x-show="status.connected">
|
|
<span class="text-sm text-gray-600">IP Address:</span>
|
|
<span class="ml-2 font-medium" x-text="status.ip_address || 'Unknown'"></span>
|
|
</div>
|
|
<div x-show="status.connected">
|
|
<span class="text-sm text-gray-600">Signal:</span>
|
|
<span class="ml-2 font-medium" x-text="status.signal + '%'"></span>
|
|
</div>
|
|
<div>
|
|
<span class="text-sm text-gray-600">AP Mode:</span>
|
|
<span x-text="status.ap_mode_active ? 'Active' : 'Inactive'"
|
|
:class="status.ap_mode_active ? 'text-green-600 font-medium' : 'text-gray-600 font-medium'"
|
|
class="ml-2"></span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3 flex gap-2">
|
|
<button @click="loadStatus()"
|
|
class="text-sm text-blue-600 hover:text-blue-800">
|
|
<i class="fas fa-sync-alt mr-1"></i>Refresh Status
|
|
</button>
|
|
<button x-show="status.connected"
|
|
@click="disconnectFromNetwork()"
|
|
:disabled="disconnecting"
|
|
class="text-sm text-red-600 hover:text-red-800 disabled:text-gray-400 disabled:cursor-not-allowed"
|
|
x-cloak>
|
|
<i class="fas fa-unlink" :class="disconnecting ? 'fa-spin' : ''"></i>
|
|
<span class="ml-1" x-text="disconnecting ? 'Disconnecting...' : 'Disconnect'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- WiFi Connection Form -->
|
|
<div class="mb-6">
|
|
<h3 class="text-sm font-medium text-gray-900 mb-4">Connect to WiFi Network</h3>
|
|
|
|
<!-- Network Selection -->
|
|
<div class="form-group mb-4" id="setting-wifi-ssid" data-setting-key="wifi.ssid">
|
|
<label for="wifi-ssid" class="block text-sm font-medium text-gray-700 mb-2">
|
|
Step 1: Select Network{{ ui.help_tip('Choose the WiFi network to join.\nClick Scan to list nearby networks, or type the name manually below if it is hidden.', 'Select Network') }}
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<select id="wifi-ssid"
|
|
x-model="selectedSSID"
|
|
@change="manualSSID = ''"
|
|
class="form-control flex-1">
|
|
<option value="">-- Select a network --</option>
|
|
<template x-for="(network, index) in networks" :key="index">
|
|
<option :value="network.ssid" x-text="network.ssid + ' (' + network.signal + '% - ' + network.security + ')'"></option>
|
|
</template>
|
|
</select>
|
|
<!-- Debug: Show network count -->
|
|
<div x-show="networks.length > 0" class="text-xs text-gray-500 self-center px-2" x-cloak>
|
|
(<span x-text="networks.length"></span> networks)
|
|
</div>
|
|
<button @click="scanNetworks()"
|
|
:disabled="scanning"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed">
|
|
<i class="fas fa-sync-alt" :class="scanning ? 'fa-spin' : ''"></i>
|
|
<span class="ml-2">Scan</span>
|
|
</button>
|
|
</div>
|
|
<!-- Show selected network -->
|
|
<div x-show="selectedSSID" class="mt-2 p-2 bg-blue-50 border border-blue-200 rounded text-sm" x-cloak>
|
|
<i class="fas fa-check-circle text-blue-600 mr-2"></i>
|
|
<span class="font-medium">Selected:</span> <span x-text="selectedSSID"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manual SSID Entry -->
|
|
<div class="form-group mb-4" id="setting-wifi-manual_ssid" data-setting-key="wifi.manual_ssid">
|
|
<label for="manual-ssid" class="block text-sm font-medium text-gray-700 mb-2">
|
|
Or Enter SSID Manually{{ ui.help_tip('Type a network name by hand when it is hidden or not shown in the scan results.\nExact spelling and capitalization matter.', 'Enter SSID Manually') }}
|
|
</label>
|
|
<input type="text"
|
|
id="manual-ssid"
|
|
x-model="manualSSID"
|
|
@input="selectedSSID = ''"
|
|
placeholder="Enter network name"
|
|
class="form-control">
|
|
</div>
|
|
|
|
<!-- Password -->
|
|
<div class="form-group mb-4" id="setting-wifi-password" data-setting-key="wifi.password">
|
|
<label for="wifi-password" class="block text-sm font-medium text-gray-700 mb-2">
|
|
Step 2: Enter Password{{ ui.help_tip('Password for the selected network.\nLeave empty if the network is open (no password required).', 'WiFi Password') }}
|
|
</label>
|
|
<input type="password"
|
|
id="wifi-password"
|
|
x-model="password"
|
|
placeholder="Enter password (leave empty for open networks)"
|
|
class="form-control">
|
|
</div>
|
|
|
|
<!-- Connect Button -->
|
|
<div class="form-group">
|
|
<button @click="connectToNetwork()"
|
|
:disabled="connecting || (!selectedSSID && !manualSSID)"
|
|
class="w-full md:w-auto px-6 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed">
|
|
<i class="fas fa-plug" :class="connecting ? 'fa-spin' : ''"></i>
|
|
<span class="ml-2" x-text="connecting ? 'Connecting...' : 'Step 3: Connect'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AP Mode Controls -->
|
|
<div class="border-t border-gray-200 pt-6">
|
|
<h3 class="text-sm font-medium text-gray-900 mb-4">Access Point Mode</h3>
|
|
<p class="text-sm text-gray-600 mb-4">
|
|
Access point mode allows you to connect to the Raspberry Pi even when it's not connected to WiFi.
|
|
</p>
|
|
|
|
<!-- Auto-Enable Toggle -->
|
|
<div class="form-group mb-4 p-4 bg-gray-50 rounded-lg border border-gray-200" id="setting-wifi-auto_enable_ap_mode" data-setting-key="wifi.auto_enable_ap_mode">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="flex-1">
|
|
<label class="text-sm font-medium text-gray-900 block mb-1">Auto-Enable AP Mode{{ ui.help_tip('Automatically start access-point mode when both WiFi and Ethernet are disconnected, so you can always reach the device to reconfigure it.\nDefault: on. When off, AP mode must be enabled manually.', 'Auto-Enable AP Mode') }}</label>
|
|
</div>
|
|
<div class="flex-shrink-0">
|
|
<button type="button"
|
|
role="switch"
|
|
aria-label="Auto-Enable AP Mode"
|
|
:aria-checked="status.auto_enable_ap_mode ? 'true' : 'false'"
|
|
@click="toggleAutoEnable(!status.auto_enable_ap_mode)"
|
|
:class="status.auto_enable_ap_mode
|
|
? 'bg-blue-600 hover:bg-blue-700'
|
|
: 'bg-gray-300 hover:bg-gray-400'"
|
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
|
<span :class="status.auto_enable_ap_mode ? 'translate-x-6' : 'translate-x-1'"
|
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2 text-xs" :class="status.auto_enable_ap_mode ? 'text-green-600' : 'text-gray-500'">
|
|
<span x-text="status.auto_enable_ap_mode ? '✓ Auto-enable is ON' : '○ Auto-enable is OFF'"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manual AP Mode Controls -->
|
|
<div class="flex gap-2">
|
|
<button @click="enableAPMode()"
|
|
:disabled="apOperating || status.ap_mode_active"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed">
|
|
<i class="fas fa-broadcast-tower" :class="apOperating ? 'fa-spin' : ''"></i>
|
|
<span class="ml-2">Enable AP Mode</span>
|
|
</button>
|
|
<button @click="disableAPMode()"
|
|
:disabled="apOperating || !status.ap_mode_active"
|
|
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:bg-gray-400 disabled:cursor-not-allowed">
|
|
<i class="fas fa-stop" :class="apOperating ? 'fa-spin' : ''"></i>
|
|
<span class="ml-2">Disable AP Mode</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Messages -->
|
|
<div x-show="message"
|
|
x-text="message"
|
|
: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>
|
|
|
|
<script>
|
|
// Ensure wifiSetup is available globally for Alpine.js
|
|
function wifiSetup() {
|
|
return {
|
|
status: {
|
|
connected: false,
|
|
ssid: null,
|
|
ip_address: null,
|
|
signal: 0,
|
|
ap_mode_active: false,
|
|
auto_enable_ap_mode: true // Default: true (safe due to grace period)
|
|
},
|
|
statusLoaded: false,
|
|
networks: [],
|
|
selectedSSID: '',
|
|
manualSSID: '',
|
|
password: '',
|
|
scanning: false,
|
|
connecting: false,
|
|
disconnecting: false,
|
|
apOperating: false,
|
|
message: '',
|
|
messageType: 'success',
|
|
|
|
init() {
|
|
// Watch networks array and update select when it changes
|
|
this.$watch('networks', () => {
|
|
this.$nextTick(() => {
|
|
this.updateSelectOptions();
|
|
});
|
|
});
|
|
},
|
|
|
|
updateSelectOptions() {
|
|
// Fallback method to manually update select if x-for doesn't work
|
|
const select = document.getElementById('wifi-ssid');
|
|
if (!select) return;
|
|
|
|
// Clear existing options except the first one
|
|
while (select.options.length > 1) {
|
|
select.remove(1);
|
|
}
|
|
|
|
// Add network options
|
|
this.networks.forEach(network => {
|
|
const option = document.createElement('option');
|
|
option.value = network.ssid;
|
|
option.textContent = `${network.ssid} (${network.signal}% - ${network.security})`;
|
|
select.appendChild(option);
|
|
});
|
|
},
|
|
|
|
async loadStatus() {
|
|
try {
|
|
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');
|
|
}
|
|
} catch (error) {
|
|
this.showMessage('Failed to load WiFi status: ' + error.message, 'error');
|
|
}
|
|
},
|
|
|
|
async scanNetworks() {
|
|
this.scanning = true;
|
|
this.message = '';
|
|
try {
|
|
const response = await fetch('/api/v3/wifi/scan');
|
|
const data = await response.json();
|
|
console.log('WiFi scan response:', data); // Debug log
|
|
if (data.status === 'success') {
|
|
// Ensure data.data is an array
|
|
const networksArray = Array.isArray(data.data) ? data.data : [];
|
|
console.log('WiFi scan found networks:', networksArray.length); // Debug log
|
|
|
|
// Set the networks array - Alpine.js will automatically update the select dropdown via x-for
|
|
this.networks = networksArray;
|
|
|
|
if (networksArray.length > 0) {
|
|
this.showMessage(`Found ${networksArray.length} networks`, 'success');
|
|
} else {
|
|
this.showMessage('No networks found. Make sure WiFi is enabled and try again.', 'warning');
|
|
}
|
|
} else {
|
|
this.showMessage(data.message || 'Failed to scan networks', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('WiFi scan error:', error); // Debug log
|
|
this.showMessage('Failed to scan networks: ' + error.message, 'error');
|
|
} finally {
|
|
this.scanning = false;
|
|
}
|
|
},
|
|
|
|
async connectToNetwork() {
|
|
const ssid = this.selectedSSID || this.manualSSID;
|
|
if (!ssid || ssid.trim() === '') {
|
|
this.showMessage('Please select or enter a network SSID', 'error');
|
|
return;
|
|
}
|
|
|
|
// Check if we're switching networks
|
|
const isSwitching = this.status.connected &&
|
|
this.status.ssid &&
|
|
this.status.ssid !== ssid.trim();
|
|
|
|
if (isSwitching) {
|
|
this.showMessage(`Switching from ${this.status.ssid} to ${ssid.trim()}...`, 'info');
|
|
}
|
|
|
|
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',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
ssid: ssid.trim(),
|
|
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
|
|
let errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
|
try {
|
|
const errorData = await response.json();
|
|
if (errorData.message) {
|
|
errorMessage = errorData.message;
|
|
}
|
|
} catch (e) {
|
|
// If we can't parse JSON, use the status text
|
|
}
|
|
this.showMessage(errorMessage, 'error');
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
const successMsg = isSwitching
|
|
? `Successfully switched to ${ssid.trim()}`
|
|
: data.message;
|
|
this.showMessage(successMsg, 'success');
|
|
// Clear form
|
|
this.password = '';
|
|
this.selectedSSID = '';
|
|
this.manualSSID = '';
|
|
// Reload status multiple times to catch connection updates
|
|
setTimeout(() => this.loadStatus(), 2000);
|
|
setTimeout(() => this.loadStatus(), 5000);
|
|
setTimeout(() => this.loadStatus(), 10000);
|
|
} else {
|
|
this.showMessage(data.message || 'Failed to connect to network', 'error');
|
|
}
|
|
} catch (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');
|
|
return;
|
|
}
|
|
|
|
this.disconnecting = true;
|
|
this.message = '';
|
|
try {
|
|
const response = await fetch('/api/v3/wifi/disconnect', {
|
|
method: 'POST'
|
|
});
|
|
|
|
// Check if response is ok before parsing JSON
|
|
if (!response.ok) {
|
|
let errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
|
try {
|
|
const errorData = await response.json();
|
|
if (errorData.message) {
|
|
errorMessage = errorData.message;
|
|
}
|
|
} catch (e) {
|
|
// If we can't parse JSON, use the status text
|
|
}
|
|
this.showMessage(errorMessage, 'error');
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
this.showMessage(data.message, 'success');
|
|
// Reload status after a delay to show updated state
|
|
setTimeout(() => this.loadStatus(), 2000);
|
|
} else {
|
|
this.showMessage(data.message || 'Failed to disconnect from network', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('WiFi disconnect error:', error);
|
|
this.showMessage('Failed to disconnect: ' + error.message, 'error');
|
|
} finally {
|
|
this.disconnecting = false;
|
|
}
|
|
},
|
|
|
|
async enableAPMode() {
|
|
this.apOperating = true;
|
|
this.message = '';
|
|
try {
|
|
const response = await fetch('/api/v3/wifi/ap/enable', {
|
|
method: 'POST'
|
|
});
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
this.showMessage(data.message, 'success');
|
|
setTimeout(() => this.loadStatus(), 1000);
|
|
} else {
|
|
this.showMessage(data.message, 'error');
|
|
}
|
|
} catch (error) {
|
|
this.showMessage('Failed to enable AP mode: ' + error.message, 'error');
|
|
} finally {
|
|
this.apOperating = false;
|
|
}
|
|
},
|
|
|
|
async disableAPMode() {
|
|
this.apOperating = true;
|
|
this.message = '';
|
|
try {
|
|
const response = await fetch('/api/v3/wifi/ap/disable', {
|
|
method: 'POST'
|
|
});
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
this.showMessage(data.message, 'success');
|
|
setTimeout(() => this.loadStatus(), 1000);
|
|
} else {
|
|
this.showMessage(data.message, 'error');
|
|
}
|
|
} catch (error) {
|
|
this.showMessage('Failed to disable AP mode: ' + error.message, 'error');
|
|
} finally {
|
|
this.apOperating = false;
|
|
}
|
|
},
|
|
|
|
async toggleAutoEnable(enabled) {
|
|
try {
|
|
const response = await fetch('/api/v3/wifi/ap/auto-enable', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
auto_enable_ap_mode: enabled
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
this.status.auto_enable_ap_mode = enabled;
|
|
this.showMessage(
|
|
enabled
|
|
? 'Auto-enable AP mode is now enabled'
|
|
: 'Auto-enable AP mode is now disabled',
|
|
'success'
|
|
);
|
|
} else {
|
|
this.showMessage(data.message, 'error');
|
|
// Revert toggle on error
|
|
this.status.auto_enable_ap_mode = !enabled;
|
|
}
|
|
} catch (error) {
|
|
this.showMessage('Failed to update auto-enable setting: ' + error.message, 'error');
|
|
// Revert toggle on error
|
|
this.status.auto_enable_ap_mode = !enabled;
|
|
}
|
|
},
|
|
|
|
showMessage(msg, type = 'success') {
|
|
this.message = msg;
|
|
this.messageType = type;
|
|
if (type === 'success') {
|
|
setTimeout(() => this.message = '', 5000);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Also make it available as a global function for Alpine.js
|
|
if (typeof window !== 'undefined') {
|
|
window.wifiSetup = wifiSetup;
|
|
}
|
|
</script>
|
|
|