feat: refine portal flow and examples

This commit is contained in:
2026-09-03 18:54:41 +10:00
parent f8b87f7865
commit 2fec33b952
42 changed files with 870 additions and 212 deletions
+4 -2
View File
@@ -268,7 +268,7 @@ struct PortalHomeCard {
struct PortalBrandState {
WiFiManagerPortalText title;
WiFiManagerPortalText identityTextOverride;
WiFiManagerPortalText homeIntro;
WiFiManagerPortalText tagline;
WiFiManagerPortalAsset logo;
WiFiManagerPortalText logoAltText;
};
@@ -915,7 +915,7 @@ class WiFiManager
// preload scanning causes AP to delay showing for users, but also caches and lets the cp load faster once its open
// my scan takes 7-10 seconds
public:
boolean _preloadwifiscan = false; // preload wifiscan if true
boolean _preloadwifiscan = true; // begin one asynchronous scan as the portal starts
unsigned int _scancachetime = 30000; // ms cache time for preload scans
protected:
@@ -963,6 +963,7 @@ protected:
void handleStationAttemptFailure(uint8_t status, const String& message);
void handleStationConnectionSuccess();
void completePortalStationAttempt(bool success, uint8_t status, const String& message);
void acknowledgePortalConnectHandoff();
void enterStationPortal();
bool hasUsableStationConnection() const;
bool isStationProfileEnabled(const WiFiManagerStationProfiles& profiles, uint8_t slot) const;
@@ -1083,6 +1084,7 @@ protected:
_cpConnectStationIp = stationIp;
_cpConnectStatus = status;
}
void wmTestCompleteProfilePortalConnectionSuccess();
void wmTestSetPortalConnectFailure(const String& message, uint8_t status = WL_CONNECT_FAILED) {
_cpConnectState = wm_cp_connect_state_t::failed;
_cpConnectMessage = message;
@@ -55,6 +55,8 @@ class WiFiManagerHandlers {
void handleApiWifiMeta(AsyncWebServerRequest *request);
void handleApiWifiSave(AsyncWebServerRequest *request);
void handleApiWifiConnectStatus(AsyncWebServerRequest *request);
void handleApiWifiConnectComplete(AsyncWebServerRequest *request);
void handleApiPortalTimeoutReset(AsyncWebServerRequest *request);
void handleApiParamsGet(AsyncWebServerRequest *request);
void handleApiParamsSave(AsyncWebServerRequest *request);
void handleApiInfo(AsyncWebServerRequest *request);
@@ -84,7 +84,7 @@ struct WiFiManagerPortalTheme {
struct WiFiManagerPortalConfig {
WiFiManagerPortalText title;
WiFiManagerPortalText identityText;
WiFiManagerPortalText homeIntro;
WiFiManagerPortalText tagline;
WiFiManagerPortalAsset logo;
WiFiManagerPortalText logoAltText;
WiFiManagerPortalTheme theme;
@@ -15,6 +15,8 @@
* GET /api/wifi/meta
* POST /api/wifi/save
* GET /api/wifi/connect-status
* POST /api/wifi/connect-complete
* POST /api/portal/timeout-reset
* GET /api/params
* POST /api/params/save
* GET /api/info
@@ -55,6 +57,8 @@ const char R_api_wifi_scan[] PROGMEM = "/api/wifi/scan";
const char R_api_wifi_meta[] PROGMEM = "/api/wifi/meta";
const char R_api_wifi_save[] PROGMEM = "/api/wifi/save";
const char R_api_wifi_connect_status[] PROGMEM = "/api/wifi/connect-status";
const char R_api_wifi_connect_complete[] PROGMEM = "/api/wifi/connect-complete";
const char R_api_portal_timeout_reset[] PROGMEM = "/api/portal/timeout-reset";
const char R_api_params[] PROGMEM = "/api/params";
const char R_api_params_save[] PROGMEM = "/api/params/save";
const char R_api_info[] PROGMEM = "/api/info";
+34 -9
View File
@@ -74,19 +74,21 @@ const char CSS_STYLE[] PROGMEM = "<style>"
".wm-icon-label{display:inline-flex;align-items:center;gap:8px;min-width:0}"
".wm-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1rem;height:1.1rem;flex:0 0 1.1rem;color:currentColor}"
".wm-icon svg{display:block;width:100%;height:100%}"
/* Hero / page chrome */
".wm-hero{margin-bottom:20px}"
".wm-hero-title{font-size:1.65rem;line-height:1.2;margin-bottom:.25em}"
".wm-hero-sub{font-size:1rem;color:var(--wm-muted);font-weight:500;margin:0}"
".wm-brand-logo{margin-bottom:12px;line-height:0}"
".wm-brand-logo svg{display:block;max-width:160px;height:auto}"
".wm-hero-intro{font-size:.95rem;color:var(--wm-muted);margin:.5em 0 0;line-height:1.45;max-width:42ch}"
/* Header / page chrome */
".wm-site-header{display:flex;align-items:center;gap:10px;min-height:40px;margin:0 0 12px}"
".wm-site-logo{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:0 0 40px;line-height:0}"
".wm-site-logo svg{display:block;width:100%;height:100%}"
".wm-site-brand-copy{min-width:0}"
".wm-site-brand-name{margin:0;font-size:.78rem;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--wm-text)}"
".wm-site-tagline{margin:2px 0 0;font-size:.82rem;line-height:1.25;color:var(--wm-muted)}"
".wm-page-head{margin-bottom:16px}"
".wm-page-head h1{font-size:1.45rem}"
".wm-page-desc{font-size:.95rem;color:var(--wm-muted);margin:.35em 0 0}"
/* Cards */
".wm-card{background:var(--wm-surface);border:1px solid var(--wm-border);border-radius:var(--wm-radius);"
"box-shadow:var(--wm-shadow);padding:18px 16px;margin-bottom:16px;text-align:left}"
".wm-scan-card,.wm-ota-card{position:relative;overflow:hidden}"
".wm-scan-results{min-height:126px}"
".wm-card-title{font-size:1rem;margin:0 0 12px;padding-bottom:10px;border-bottom:1px solid var(--wm-border);"
"color:var(--wm-text)}"
".wm-card-subtitle{margin:-4px 0 12px;font-size:.9rem;line-height:1.45;color:var(--wm-muted)}"
@@ -120,6 +122,31 @@ const char CSS_STYLE[] PROGMEM = "<style>"
".wm-checkbox-row{display:flex;align-items:center;gap:8px;margin:12px 0;font-size:.92rem}"
".wm-checkbox-row label{display:inline;margin:0;font-weight:500;color:var(--wm-text)}"
".wm-form-actions{margin-top:16px}"
".wm-wifi-details-card{position:relative}"
".wm-wifi-save-overlay{position:fixed;inset:0;z-index:10002;display:none;align-items:center;justify-content:center;padding:18px;"
"box-sizing:border-box;background:rgba(15,23,42,.58)}"
".wm-wifi-save-panel{display:flex;align-items:center;gap:14px;width:100%;max-width:340px;padding:18px;border:1px solid var(--wm-border);"
"border-radius:var(--wm-radius-sm);background:var(--wm-surface);box-shadow:var(--wm-shadow);text-align:left}"
".wm-wifi-save-panel strong{display:block;font-size:1rem;color:var(--wm-text)}"
".wm-wifi-save-panel p{margin:4px 0 0;font-size:.9rem;line-height:1.45;color:var(--wm-muted)}"
".wm-operation-overlay{position:absolute;inset:0;z-index:5;display:none;align-items:center;justify-content:center;padding:16px;background:rgba(15,23,42,.56)}"
".wm-operation-panel{display:flex;align-items:center;gap:12px;width:100%;max-width:310px;padding:16px;border:1px solid var(--wm-border);border-radius:var(--wm-radius-sm);background:var(--wm-surface);box-shadow:var(--wm-shadow);text-align:left}"
".wm-operation-panel strong{display:block;font-size:.98rem;color:var(--wm-text)}"
".wm-operation-panel p{margin:3px 0 0;font-size:.88rem;line-height:1.4;color:var(--wm-muted)}"
".wm-operation-progress{margin-top:10px}"
".wm-operation-progress-track{height:7px;overflow:hidden;border-radius:999px;background:var(--wm-border)}"
".wm-operation-progress-track span{display:block;width:0;height:100%;border-radius:inherit;background:var(--wm-brand);transition:width .15s ease}"
".wm-operation-progress-label{display:block;margin-top:4px;font-size:.8rem;font-weight:700;color:var(--wm-muted);text-align:right}"
".wm-portal-timeout-row{display:flex;align-items:stretch;gap:8px;margin:0 0 12px}"
".wm-portal-timeout-row .wm-status{flex:1;margin:0}"
".wm-icon-button{display:inline-flex;align-items:center;justify-content:center;width:42px;min-height:42px;padding:0;border:1px solid var(--wm-border);border-radius:var(--wm-radius-sm);background:var(--wm-bg);color:var(--wm-text);cursor:pointer}"
".wm-icon-button:hover{border-color:var(--wm-brand);color:var(--wm-brand)}"
".wm-icon-button:disabled{opacity:.5;cursor:not-allowed}"
".wm-icon-button .wm-icon{width:1.05rem;height:1.05rem}"
".wm-inline-link{display:inline-block;margin-top:12px;font-size:.92rem}"
".wm-spinner{width:24px;height:24px;flex:0 0 24px;border:3px solid var(--wm-border);border-top-color:var(--wm-brand);border-radius:50%;animation:wm-spin .8s linear infinite}"
"@keyframes wm-spin{to{transform:rotate(360deg)}}"
"@media (prefers-reduced-motion:reduce){.wm-spinner{animation:none;border-right-color:var(--wm-brand)}}"
/* Buttons */
".wm-btn{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 18px;"
"font-size:1rem;font-weight:600;font-family:inherit;border-radius:var(--wm-radius-sm);border:0;cursor:pointer;"
@@ -180,8 +207,6 @@ const char CSS_STYLE[] PROGMEM = "<style>"
".wm-kv dd:last-of-type{border-bottom:0}"
/* Feedback areas */
"#wm-wifi-msg,#wm-param-msg{font-size:.9rem;margin-top:12px;min-height:1.2em;color:var(--wm-muted)}"
"#wm-ota-log{font-size:.8rem;background:var(--wm-bg);padding:12px;border-radius:var(--wm-radius-sm);"
"border:1px solid var(--wm-border);overflow:auto;max-height:160px}"
// invert legacy body
"body.invert{background-color:var(--wm-bg)}"
"body.invert .msg{border-color:var(--wm-border)}"
+354 -115
View File
@@ -120,7 +120,7 @@ function startPortalTimeoutCountdown(initialSeconds){
function render(){
if(!el)return;
el.className='wm-status'+(levelClass(remaining)?' '+levelClass(remaining):'');
el.textContent='Captive portal timeout: '+formatDurationSeconds(remaining);
el.textContent='Portal closes in '+formatDurationSeconds(remaining);
}
render();
_wmPortalTimeoutTimer=setInterval(function(){
@@ -260,6 +260,28 @@ function rowsWithoutKeys(rows,blocked){
return out;
}
function portalHeader(){
var brand=boot.brand||{};
var ctx=boot.context||{};
var identity=esc(ctx.identityText||brand.title||'WiFiManager');
var logoAlt=esc(brand.logoAltText||'');
var h="<header class='wm-site-header'>";
if(brand.logoSvg){h+="<div class='wm-site-logo'"+(logoAlt?" role='img' aria-label='"+logoAlt+"'":"")+">"+brand.logoSvg+"</div>";}
h+="<div class='wm-site-brand-copy'><p class='wm-site-brand-name'>"+identity+"</p>";
if(brand.tagline){h+="<p class='wm-site-tagline'>"+esc(brand.tagline)+"</p>";}
h+="</div></header>";
return h;
}
function applyPortalFavicon(){
var brand=boot.brand||{};
if(!brand.logoSvg)return;
var icon=document.querySelector("link[rel~='icon']");
if(!icon){icon=document.createElement('link');icon.rel='icon';document.head.appendChild(icon);}
icon.type='image/svg+xml';
icon.href='data:image/svg+xml,'+encodeURIComponent(brand.logoSvg);
}
function navBar(active){
active=active||'home';
var pages=boot.pages||{};
@@ -269,11 +291,11 @@ function navBar(active){
var ps=pages.setup||{};
var pi=pages.info||{};
var pu=pages.update||{};
var h="<nav class='wm-nav' aria-label='Configuration'>";
h+="<a class='wm-nav-link"+(active==='home'?' wm-nav-link--active':'')+"' href='#/'>"+labelWithIcon('home','Home')+"</a>";
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>"+labelWithIcon('wifi','WiFi')+"</a>";
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>"+labelWithIcon('setup','Setup')+"</a>";}
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>"+labelWithIcon('info','Info')+"</a>";}
var h=portalHeader()+"<nav class='wm-nav' aria-label='Configuration'>";
h+="<a class='wm-nav-link"+(active==='home'?' wm-nav-link--active':'')+"' href='#/'>"+labelWithIcon('home','Overview')+"</a>";
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>"+labelWithIcon('wifi','Wi-Fi')+"</a>";
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>"+labelWithIcon('setup','Settings')+"</a>";}
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>"+labelWithIcon('info','Device')+"</a>";}
if(pu.visible){h+="<a class='wm-nav-link"+(active==='update'?' wm-nav-link--active':'')+"' href='#/update'>"+labelWithIcon('update','Update')+"</a>";}
if(actions.back&&actions.back.visible){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>"+labelWithIcon('back','Back')+"</a>";}
h+="</nav>";
@@ -311,30 +333,91 @@ function renderExtraHomeCards(cards){
return out;
}
function renderHomeStatusDetails(info){
var st=info.status||{};
var rows=[];
if(st.connected!==undefined)rows.push({label:'Wi-Fi',value:st.connected?'Connected':'Not connected'});
if(st.ssid)rows.push({label:'Network',value:st.ssid});
if(st.connected&&st.stationIp&&st.stationIp!=='0.0.0.0'&&st.stationIp!=='(IP unset)')rows.push({label:'Address',value:st.stationIp});
else if(st.apIp)rows.push({label:'Portal',value:st.apIp});
var device=info.device||[];
var wanted={uptime:true,freeheap:true};
for(var i=0;i<device.length;i++){
if(device[i]&&wanted[device[i].key]){
rows.push({label:device[i].label||device[i].key,value:device[i].value||''});
}
}
if(!rows.length)return"<p class='wm-lead'>Device details are unavailable.</p>";
var html="<dl class='wm-kv wm-home-status-details'>";
for(var j=0;j<rows.length;j++){
html+="<dt>"+esc(rows[j].label)+"</dt>";
html+="<dd>"+esc(rows[j].value)+"</dd>";
}
return html+="</dl>";
}
function loadHomeStatus(){
var box=$('wm-home-status-details');
if(!box)return;
api('/api/info').then(function(res){
var info={};
try{info=JSON.parse(res.body);}catch(e){}
box.innerHTML=renderHomeStatusDetails(info);
}).catch(function(){
box.innerHTML="<p class='wm-lead'>Unable to load device details.</p>";
});
}
function resetPortalTimeout(){
var button=$('wm-reset-portal-timeout');
if(button)button.disabled=true;
api('/api/portal/timeout-reset',{method:'POST'}).then(function(res){
var data={};
try{data=JSON.parse(res.body);}catch(e){}
if(!res.ok){
showToast(data.message||'Unable to reset portal timeout.',true);
return;
}
startPortalTimeoutCountdown(data.timeoutSecondsRemaining||0);
showToast('Portal timeout reset.',false);
}).catch(function(){
showToast('Unable to reset portal timeout.',true);
}).then(function(){
if(button)button.disabled=false;
});
}
function viewHome(){
var brand=boot.brand||{};
var ctx=boot.context||{};
var t=esc(brand.title||'WiFiManager');
var ident=ctx.identityText?esc(ctx.identityText):'';
var title=esc(brand.title||'WiFiManager');
var summary=ctx.statusSummary||'';
var timeoutSeconds=ctx.portalTimeoutSecondsRemaining||0;
var html=navBar('home');
html+="<header class='wm-hero'>";
var logoAlt=esc(brand.logoAltText||'');
if(brand.logoSvg){html+="<div class='wm-brand-logo'"+(logoAlt?" role='img' aria-label='"+logoAlt+"'":"")+">"+brand.logoSvg+"</div>";}
html+="<h1 class='wm-hero-title'>"+t+"</h1>";
if(ident){html+="<p class='wm-hero-sub'>"+ident+"</p>";}
if(brand.homeIntro){html+="<p class='wm-hero-intro'>"+esc(brand.homeIntro)+"</p>";}
html+="</header>";
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
if(summary){html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";}
if(timeoutSeconds>0){html+="<p class='wm-status' id='wm-portal-timeout'></p>";}
html+="<p class='wm-lead'>Use <strong>WiFi</strong> to scan networks and connect. Open <strong>Info</strong> for addresses and firmware actions.</p>";
html+="</div>";
html+="<div class='wm-page-head'><h1>"+title+"</h1></div>";
html+="<div class='wm-card'>"+cardTitleHtml('status','Device status');
if(summary){
html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";
}
if(timeoutSeconds>0){
html+="<div class='wm-portal-timeout-row'>";
html+="<p class='wm-status' id='wm-portal-timeout'></p>";
html+="<button type='button' class='wm-icon-button'";
html+=" id='wm-reset-portal-timeout' aria-label='Reset portal timeout'";
html+=" title='Reset portal timeout'>"+iconSvg('restart')+"</button></div>";
}
html+="<div id='wm-home-status-details'>";
html+="<p class='wm-lead'>Loading device details…</p></div>";
html+="<a class='wm-inline-link' href='#/info'>View full device details</a></div>";
html+=renderExtraHomeCards(boot.extraHomeCards||[]);
html+=deviceActionsHtml();
setView(html);
if(timeoutSeconds>0)startPortalTimeoutCountdown(timeoutSeconds);
if(timeoutSeconds>0){
startPortalTimeoutCountdown(timeoutSeconds);
var reset=$('wm-reset-portal-timeout');
if(reset)reset.addEventListener('click',resetPortalTimeout);
}
loadHomeStatus();
}
// --- HTTP ---
@@ -366,27 +449,55 @@ function stopWifiScanPolling(){
setScanRefreshEnabled(true);
}
function setWifiScanProgress(visible,title,detail){
var overlay=$('wm-wifi-scan-overlay');
var titleEl=$('wm-wifi-scan-title');
var detailEl=$('wm-wifi-scan-detail');
if(!overlay)return;
if(titleEl)titleEl.textContent=title||'Scanning Wi-Fi';
if(detailEl)detailEl.textContent=detail||'';
overlay.style.display=visible?'flex':'none';
overlay.setAttribute('aria-hidden',visible?'false':'true');
}
function startWifiScanPolling(box){
stopWifiScanPolling();
setScanRefreshEnabled(false);
setWifiScanProgress(true,'Scanning Wi-Fi','Looking for nearby networks…');
var token=++_wmWifiScanPollToken;
var unavailableDeadline=Date.now()+20000;
function retryAfterTemporaryRadioLoss(){
if(Date.now()<unavailableDeadline){
setWifiScanProgress(true,'Scanning Wi-Fi','Waiting for the device radio to return…');
_wmWifiScanPollTimer=setTimeout(poll,1000);
return true;
}
return false;
}
function poll(){
api('/api/wifi/scan-status').then(function(res){
if(token!==_wmWifiScanPollToken)return;
var d={};
try{d=JSON.parse(res.body);}catch(e){return;}
if(!res.ok)throw new Error('scan status unavailable');
var data={};
try{data=JSON.parse(res.body);}catch(e){
if(retryAfterTemporaryRadioLoss())return;
throw e;
}
box=getScanResultsBox()||box;
if(box)box.innerHTML=renderScanList(d);
if(!d.scanning){
if(!data.scanning){
if(box)box.innerHTML=renderScanList(data);
setWifiScanProgress(false);
stopWifiScanPolling();
return;
}
_wmWifiScanPollTimer=setTimeout(poll,800);
}).catch(function(){
if(token!==_wmWifiScanPollToken)return;
if(retryAfterTemporaryRadioLoss())return;
stopWifiScanPolling();
setWifiScanProgress(false);
box=getScanResultsBox()||box;
if(box)box.innerHTML=renderScanError('Portal became unreachable while scanning.');
if(box)box.innerHTML=renderScanError('The portal did not return after scanning.');
});
}
poll();
@@ -440,21 +551,23 @@ function wifiRefresh(){
var box=getScanResultsBox();
if(!box)return;
setScanRefreshEnabled(false);
box.innerHTML="<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>Starting scan…</p></div>";
setWifiScanProgress(true,'Preparing Wi-Fi scan','Starting a scan…');
api('/api/wifi/scan',{method:'POST'}).then(function(res){
box=getScanResultsBox()||box;
if(!res.ok){
setScanRefreshEnabled(true);
if(box)box.innerHTML=renderScanError('Failed to start WiFi scan.');
showToast('Failed to start WiFi scan.',true);
setWifiScanProgress(false);
if(box)box.innerHTML=renderScanError('Failed to start Wi-Fi scan.');
showToast('Failed to start Wi-Fi scan.',true);
return;
}
startWifiScanPolling(box);
}).catch(function(){
stopWifiScanPolling();
setWifiScanProgress(false);
box=getScanResultsBox()||box;
if(box)box.innerHTML=renderScanError('Failed to reach portal while starting scan.');
showToast('Failed to reach portal while starting scan.',true);
if(box)box.innerHTML=renderScanError('Failed to reach the portal while starting a scan.');
showToast('Failed to reach the portal while starting a scan.',true);
});
}
@@ -530,57 +643,76 @@ function renderStationProfileFields(profiles){
}
function viewWifi(){
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading WiFi options…</p></div>");
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading Wi-Fi options…</p></div>");
api('/api/wifi/meta').then(function(res){
try{var m=JSON.parse(res.body);}catch(e){m={};}
var meta={};
try{meta=JSON.parse(res.body);}catch(e){}
var html=navBar('wifi');
html+="<div class='wm-page-head'><h1>WiFi</h1><p class='wm-page-desc'>Pick a network below, confirm SSID and password, then connect.</p></div>";
html+="<div class='wm-card'>"+cardTitleHtml('scan','Networks nearby');
html+="<div id='wm-scan-results'></div>";
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button></div>";
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
if(Array.isArray(m.profiles) && m.profiles.length){
html+=renderStationProfileFields(m.profiles);
html+="<div class='wm-page-head'><h1>Connect to Wi-Fi</h1>";
html+="<p class='wm-page-desc'>Choose a network, then save and connect.</p></div>";
html+="<div class='wm-card wm-scan-card'>"+cardTitleHtml('scan','Networks nearby');
html+="<div id='wm-scan-results' class='wm-scan-results'>";
html+="<p class='wm-lead'>Preparing nearby networks…</p></div>";
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block'";
html+=" id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button>";
html+="<div id='wm-wifi-scan-overlay' class='wm-operation-overlay'";
html+=" role='status' aria-live='polite' aria-atomic='true' aria-hidden='true'>";
html+="<div class='wm-operation-panel'><span class='wm-spinner' aria-hidden='true'></span>";
html+="<div><strong id='wm-wifi-scan-title'>Scanning Wi-Fi</strong>";
html+="<p id='wm-wifi-scan-detail'>Looking for nearby networks…</p></div></div></div></div>";
html+="<div class='wm-card wm-wifi-details-card'>"+cardTitleHtml('wifi','Network details');
if(Array.isArray(meta.profiles)&&meta.profiles.length){
html+=renderStationProfileFields(meta.profiles);
html+="<form class='wm-wifi-data-form' id='wm-wifi-form'>";
}else{
html+="<form class='wm-wifi-data-form' id='wm-wifi-form'>";
html+=renderFieldList(m.wifiFields||[]);
var wf=m.wifiFields||[];
var hasPass=false;
var i;
for(i=0;i<wf.length;i++){if(wf[i]&&wf[i].id==='p'){hasPass=true;break;}}
if(hasPass){
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/> <label for='wm-showpass'>Show password</label></div>";
html+=renderFieldList(meta.wifiFields||[]);
var wifiFields=meta.wifiFields||[];
var hasPassword=false;
for(var i=0;i<wifiFields.length;i++){
if(wifiFields[i]&&wifiFields[i].id==='p'){hasPassword=true;break;}
}
if(hasPassword){
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/>";
html+=" <label for='wm-showpass'>Show password</label></div>";
}
}
html+=renderFieldList(m.staticFields||[]);
html+=renderFieldList(m.params||[]);
html+=renderFieldList(meta.staticFields||[]);
html+=renderFieldList(meta.params||[]);
html+="</form>";
html+="<div class='wm-form-actions'><button type='button' class='wm-btn wm-btn--primary wm-btn--block' data-wm-wifi-action='connect'>"+labelWithIcon('connect','Save and connect')+"</button>";
if(Array.isArray(m.profiles) && m.profiles.length){
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' data-wm-wifi-action='save'>Save for later</button>";
html+="<div class='wm-form-actions'><button type='button'";
html+=" class='wm-btn wm-btn--primary wm-btn--block' data-wm-wifi-action='connect'>";
html+=labelWithIcon('connect','Save and connect')+"</button>";
if(Array.isArray(meta.profiles)&&meta.profiles.length){
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block'";
html+=" data-wm-wifi-action='save'>Save for later</button>";
}
html+="</div>";
html+="<div id='wm-wifi-msg'></div></div>";
html+="</div><div id='wm-wifi-msg'></div></div>";
html+="<div id='wm-wifi-save-overlay' class='wm-wifi-save-overlay'";
html+=" role='status' aria-live='polite' aria-atomic='true' aria-hidden='true'>";
html+="<div class='wm-wifi-save-panel'><span class='wm-spinner' aria-hidden='true'></span>";
html+="<div><strong id='wm-wifi-save-title'>Saving settings</strong>";
html+="<p id='wm-wifi-save-detail'>Please wait…</p></div></div></div>";
setView(html);
bindWifiViewEvents();
setScanRefreshEnabled(true);
api('/api/wifi/scan-status').then(function(r2){
try{var d=JSON.parse(r2.body);}catch(e){d={};}
api('/api/wifi/scan-status').then(function(scanRes){
var scan={};
try{scan=JSON.parse(scanRes.body);}catch(e){}
var box=getScanResultsBox();
if(shouldAutoStartWifiScan(d)){
if(shouldAutoStartWifiScan(scan)){
wifiRefresh();
return;
}
if(box)box.innerHTML=renderScanList(d);
if(d&&d.scanning){startWifiScanPolling(box);}
if(box)box.innerHTML=renderScanList(scan);
if(scan&&scan.scanning)startWifiScanPolling(box);
}).catch(function(){
var box=getScanResultsBox();
if(box)box.innerHTML=renderScanError('Failed to reach portal while loading scan status.');
if(box)box.innerHTML=renderScanError('Failed to load nearby networks.');
setScanRefreshEnabled(true);
});
}).catch(function(){
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Failed to load WiFi options. Reconnect to the portal and try again.</p></div>");
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Failed to load Wi-Fi options. Reconnect to the portal and try again.</p></div>");
});
}
@@ -588,6 +720,33 @@ function viewWifi(){
var _wmWifiConnectPollTimer=null;
var _wmWifiConnectPollToken=0;
var _wmWifiConnectPending=false;
var _wmWifiSaveInFlight=false;
function setWifiSaveControlsDisabled(disabled){
var controls=document.querySelectorAll('.wm-wifi-data-form input,.wm-wifi-data-form select,.wm-wifi-data-form textarea,[data-wm-wifi-action],#wm-refresh-scan');
for(var i=0;i<controls.length;i++)controls[i].disabled=disabled;
}
function setWifiSaveProgress(visible,title,detail){
var overlay=$('wm-wifi-save-overlay'),titleEl=$('wm-wifi-save-title'),detailEl=$('wm-wifi-save-detail');
if(!overlay)return;
if(titleEl)titleEl.textContent=title||'Saving settings';
if(detailEl)detailEl.textContent=detail||'';
overlay.style.display=visible?'flex':'none';
overlay.setAttribute('aria-hidden',visible?'false':'true');
}
function beginWifiSave(title,detail){
_wmWifiSaveInFlight=true;
setWifiSaveControlsDisabled(true);
setWifiSaveProgress(true,title,detail);
}
function finishWifiSave(){
_wmWifiSaveInFlight=false;
setWifiSaveControlsDisabled(false);
setWifiSaveProgress(false);
}
function stopWifiConnectPolling(){
_wmWifiConnectPollToken++;
@@ -597,47 +756,72 @@ function stopWifiConnectPolling(){
}
}
function completeWifiHandoff(data,attempt){
attempt=attempt||0;
api('/api/wifi/connect-complete',{method:'POST'}).then(function(res){
if(!res.ok){
if(attempt<2){setTimeout(function(){completeWifiHandoff(data,attempt+1);},500);return;}
throw new Error('handoff acknowledgement rejected');
}
if(data.redirectUrl){
setTimeout(function(){window.location.href=data.redirectUrl;},1200);
}else{
location.hash='#/';
}
}).catch(function(){
if(attempt<2){
setTimeout(function(){completeWifiHandoff(data,attempt+1);},500);
return;
}
setWifiSaveProgress(true,'Wi-Fi connected','Open the device address shown below.');
showToast('Wi-Fi connected, but automatic redirect was unavailable.',true);
});
}
function pollWifiConnectStatus(){
stopWifiConnectPolling();
var token=++_wmWifiConnectPollToken;
function poll(){
api('/api/wifi/connect-status').then(function(res){
if(token!==_wmWifiConnectPollToken)return;
var j={};
var data={};
var msg=$('wm-wifi-msg');
try{j=JSON.parse(res.body);}catch(e){}
if(msg&&j.message){
msg.innerHTML=esc(j.message);
}
if(j.state==='success'){
try{data=JSON.parse(res.body);}catch(e){}
if(msg&&data.message)msg.innerHTML=esc(data.message);
if(data.state==='success'){
stopWifiConnectPolling();
_wmWifiConnectPending=false;
if(msg && j.stationIp){
msg.innerHTML="Connected to WiFi. Redirecting to <a href='"+esc(j.redirectUrl||("http://"+j.stationIp+"/"))+"'>"+esc(j.stationIp)+"</a>…";
if(!data.stationIp){
finishWifiSave();
if(msg)msg.innerHTML=esc(data.message||'Settings saved.');
showToast(data.message||'Settings saved.',false);
return;
}
showToast(j.message||'WiFi connected',false);
if(j.redirectUrl){
setTimeout(function(){ window.location.href=j.redirectUrl; }, 1400);
}else{
location.hash='#/';
var destination=data.redirectUrl||'';
setWifiSaveProgress(true,'Connected to Wi-Fi','Opening '+data.stationIp+'…');
if(msg){
msg.innerHTML="Connected. Opening <a href='"+esc(destination||('http://'+data.stationIp+'/'))+"'>";
msg+=esc(data.stationIp)+"</a>…";
}
}else if(j.state==='failed'){
showToast(data.message||'Wi-Fi connected',false);
completeWifiHandoff(data);
}else if(data.state==='failed'){
stopWifiConnectPolling();
_wmWifiConnectPending=false;
showToast(j.message||'WiFi connect failed',true);
finishWifiSave();
showToast(data.message||'Wi-Fi connection failed',true);
}else{
setWifiSaveProgress(true,'Connecting to Wi-Fi',data.message||'Waiting for the device to join the selected network…');
_wmWifiConnectPollTimer=setTimeout(poll,700);
}
}).catch(function(){
if(token!==_wmWifiConnectPollToken)return;
stopWifiConnectPolling();
finishWifiSave();
var msg=$('wm-wifi-msg');
if(msg){
msg.innerHTML=_wmWifiConnectPending
? 'Portal became unreachable after saving. If the device joined your WiFi network, reconnect to it there.'
: 'Portal became unreachable while checking connection status.';
}
if(msg)msg.innerHTML='Unable to check Wi-Fi connection status. Try again.';
_wmWifiConnectPending=false;
showToast('Unable to check Wi-Fi connection status.',true);
});
}
poll();
@@ -652,9 +836,11 @@ function collectWifiFormData(){
}
function submitWifi(action){
if(_wmWifiSaveInFlight)return false;
var fd=collectWifiFormData();
if(action)fd.append('stationAction',action);
var msg=$('wm-wifi-msg');
beginWifiSave(action==='save'?'Saving settings':'Saving and connecting',action==='save'?'Saving these settings for a later connection...':'Saving settings and preparing the WiFi connection...');
if(msg)msg.innerHTML='Submitting...';
api('/api/wifi/save',{method:'POST',body:fd}).then(function(res){
var j={};
@@ -664,8 +850,19 @@ function submitWifi(action){
}
if(res.status===202){
_wmWifiConnectPending=true;
setWifiSaveProgress(true,'Connecting to WiFi',(j&&j.message)?j.message:'Waiting for the device to join the selected network...');
pollWifiConnectStatus();
}else if(res.ok){
finishWifiSave();
showToast((j&&j.message)?j.message:'Settings saved.',false);
}else{
finishWifiSave();
showToast((j&&j.message)?j.message:'Unable to save settings.',true);
}
}).catch(function(){
finishWifiSave();
if(msg)msg.innerHTML='Unable to reach the portal while saving. Please try again.';
showToast('Unable to reach the portal while saving.',true);
});
return false;
}
@@ -686,7 +883,7 @@ function viewInfo(){
api('/api/info').then(function(res){
try{var d=JSON.parse(res.body);}catch(e){d={};}
var html=navBar('info');
html+="<div class='wm-page-head'><h1>Device info</h1><p class='wm-page-desc'>Connection status, hardware, and actions.</p></div>";
html+="<div class='wm-page-head'><h1>Device</h1><p class='wm-page-desc'>Connection status, hardware, and actions.</p></div>";
var st=d.status;
if(st && typeof st==='object' && !Array.isArray(st)){
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
@@ -755,7 +952,7 @@ function viewSetup(){
api('/api/params').then(function(res){
try{var d=JSON.parse(res.body);}catch(e){d={};}
var html=navBar('setup');
html+="<div class='wm-page-head'><h1>Setup</h1><p class='wm-page-desc'>Custom parameters stored on this device.</p></div>";
html+="<div class='wm-page-head'><h1>Settings</h1><p class='wm-page-desc'>Custom parameters stored on this device.</p></div>";
html+="<div class='wm-card'><form id='wm-param-form'>";
html+=renderFieldList(d.params||[]);
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('save','Save parameters')+"</button></div>";
@@ -776,59 +973,100 @@ window.portalParamSave=function(ev){
return false;
};
function setOtaControlsDisabled(disabled){
var controls=document.querySelectorAll('#wm-ota-form input,#wm-ota-form button');
for(var i=0;i<controls.length;i++)controls[i].disabled=disabled;
}
function setOtaProgress(visible,title,detail,percent){
var overlay=$('wm-ota-overlay');
var titleEl=$('wm-ota-title');
var detailEl=$('wm-ota-detail');
var progress=$('wm-ota-progress');
var bar=$('wm-ota-progress-bar');
var label=$('wm-ota-progress-label');
if(!overlay)return;
if(titleEl)titleEl.textContent=title||'Uploading firmware';
if(detailEl)detailEl.textContent=detail||'';
var hasPercent=typeof percent==='number';
if(progress)progress.style.display=hasPercent?'block':'none';
if(hasPercent){
var value=Math.max(0,Math.min(100,Math.round(percent)));
if(bar)bar.style.width=value+'%';
if(label)label.textContent=value+'%';
}
overlay.style.display=visible?'flex':'none';
overlay.setAttribute('aria-hidden',visible?'false':'true');
}
function viewUpdate(){
var pages=boot.pages||{};
var pupd=pages.update||{};
var update=pages.update||{};
var html=navBar('update');
html+="<div class='wm-page-head'><h1>Firmware update</h1><p class='wm-page-desc'>Flash a new firmware build (.bin). The device reboots when done.</p></div>";
if(!pupd.visible){
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this configuration.</p></div>";
html+="<div class='wm-page-head'><h1>Update firmware</h1>";
html+="<p class='wm-page-desc'>Upload a firmware build for this device.</p></div>";
if(!update.visible){
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this device.</p></div>";
setView(html);
return;
}
html+="<div class='wm-card'><form id='wm-ota-form'>";
html+="<div class='wm-card wm-ota-card'><form id='wm-ota-form'>";
html+="<div class='wm-field'><label for='wm-ota-file'>Firmware file</label>";
html+="<input type='file' id='wm-ota-file' name='update' accept='.bin,.bin.gz'/></div>";
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('upload','Upload firmware')+"</button></div>";
html+="</form>";
html+="<p class='wm-lead' style='margin-top:14px;margin-bottom:8px'>Choose the correct binary for this chip. Upload progress appears below.</p>";
html+="<pre id='wm-ota-log'></pre></div>";
html+="<div class='wm-form-actions'><button type='submit'";
html+=" class='wm-btn wm-btn--primary wm-btn--block'>";
html+=labelWithIcon('upload','Upload firmware')+"</button></div></form>";
html+="<p class='wm-lead'>Use the binary built for this device. It will restart after a successful update.</p>";
html+="<div id='wm-ota-overlay' class='wm-operation-overlay'";
html+=" role='status' aria-live='polite' aria-atomic='true' aria-hidden='true'>";
html+="<div class='wm-operation-panel'><span class='wm-spinner' aria-hidden='true'></span><div>";
html+="<strong id='wm-ota-title'>Uploading firmware</strong>";
html+="<p id='wm-ota-detail'>Preparing upload…</p>";
html+="<div id='wm-ota-progress' class='wm-operation-progress' style='display:none'>";
html+="<div class='wm-operation-progress-track'><span id='wm-ota-progress-bar'></span></div>";
html+="<span id='wm-ota-progress-label'>0%</span></div></div></div></div></div>";
setView(html);
}
window.portalOtaSubmit=function(ev){
ev.preventDefault();
var fi=$('wm-ota-file');
var logEl=$('wm-ota-log');
if(!fi||!fi.files||!fi.files[0]){
if(logEl)logEl.textContent='Choose a firmware file first.';
var fileInput=$('wm-ota-file');
if(!fileInput||!fileInput.files||!fileInput.files[0]){
showToast('Choose a firmware file first.',true);
return false;
}
if(logEl)logEl.textContent='Uploading…';
setOtaControlsDisabled(true);
setOtaProgress(true,'Uploading firmware','Preparing upload…');
var xhr=new XMLHttpRequest();
xhr.upload.onprogress=function(e){
if(!logEl||!e.lengthComputable)return;
logEl.textContent='Uploading… '+Math.round(100*e.loaded/e.total)+'%';
xhr.upload.onprogress=function(event){
if(!event.lengthComputable)return;
var percent=100*event.loaded/event.total;
setOtaProgress(true,'Uploading firmware','Transferring firmware…',percent);
};
xhr.onreadystatechange=function(){
if(xhr.readyState!==4)return;
var t=xhr.responseText||'';
if(logEl)logEl.textContent=t;
var httpOk=xhr.status>=200&&xhr.status<300;
try{
var j=JSON.parse(t);
if(j&&j.message){
if(logEl)logEl.textContent=j.message;
showToast(j.message,j.ok===false);
return;
}
}catch(e){}
showToast(httpOk?'Upload finished.':('HTTP '+xhr.status),!httpOk);
var text=xhr.responseText||'';
var data={};
try{data=JSON.parse(text);}catch(e){}
var ok=xhr.status>=200&&xhr.status<300&&data.ok!==false;
if(ok){
setOtaProgress(true,'Firmware updated','Restarting device…',100);
showToast(data.message||'Firmware updated. Restarting device…',false);
return;
}
setOtaProgress(false);
setOtaControlsDisabled(false);
showToast(data.message||('Upload failed (HTTP '+xhr.status+').'),true);
};
xhr.onerror=function(){
setOtaProgress(false);
setOtaControlsDisabled(false);
showToast('Upload failed before the device responded.',true);
};
xhr.open('POST','/u');
var fd=new FormData();
fd.append('update',fi.files[0]);
xhr.send(fd);
var formData=new FormData();
formData.append('update',fileInput.files[0]);
xhr.send(formData);
return false;
};
@@ -853,6 +1091,7 @@ function route(){
}
function bootPortal(){
applyPortalFavicon();
route();
try{document.dispatchEvent(new CustomEvent('wm:ready',{detail:{boot:boot}}));}catch(e){}
}
+30 -3
View File
@@ -28,6 +28,10 @@ namespace {
constexpr size_t kMaxHostnameLength = 32;
constexpr uint8_t kSoftApStartMaxAttempts = 3;
constexpr unsigned long kStationProfileSwitchDelayMs = 2000UL; // ESP WiFi needs time to leave a failed association.
// A portal client acknowledges the successful station hand-off before the AP
// is closed. The fallback protects headless/captive clients that disappear.
constexpr unsigned long kPortalSuccessHandoffFallbackMs = 12000UL;
constexpr unsigned long kPortalSuccessHandoffAcknowledgedDelayMs = 700UL;
bool isValidHostnameChar(char c) {
return isAlphaNumeric(c) || c == '-';
@@ -505,6 +509,14 @@ void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, con
_cpConnectState = success ? wm_cp_connect_state_t::success : wm_cp_connect_state_t::failed;
if (success) {
// Profile-backed provisioning reaches this path rather than
// processPortalConnect(). Keep the AP available until the SPA has read
// the station address and acknowledged its redirect hand-off.
if (_disableConfigPortal && (configPortalActive || webPortalActive)) {
_cpConnectCloseAt = millis() + kPortalSuccessHandoffFallbackMs;
_cpConnectState = wm_cp_connect_state_t::success_waiting_close;
return;
}
if (_savewificallback != NULL) {
_savewificallback();
}
@@ -518,6 +530,22 @@ void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, con
}
}
void WiFiManager::acknowledgePortalConnectHandoff() {
if (_cpConnectState != wm_cp_connect_state_t::success_waiting_close) {
return;
}
_cpConnectCloseAt = millis() + kPortalSuccessHandoffAcknowledgedDelayMs;
}
#ifdef UNIT_TEST
void WiFiManager::wmTestCompleteProfilePortalConnectionSuccess() {
_stationCandidateFromPortal = true;
configPortalActive = true;
_disableConfigPortal = true;
completePortalStationAttempt(true, WL_CONNECTED, F("WiFi connected"));
}
#endif
void WiFiManager::handleStationConnectionSuccess() {
const uint8_t slot = _stationStatus.attemptedSlot;
const bool wasCandidate = _stationCandidateActive;
@@ -1177,7 +1205,6 @@ void WiFiManager::queuePortalConnect(const String& ssid, const String& pass) {
void WiFiManager::processPortalConnect() {
const unsigned long now = millis();
const unsigned long successHandoffDelayMs = 2500UL;
auto finalizePortalConnectSuccess = [&]() {
_cpConnectState = wm_cp_connect_state_t::success;
if (_savewificallback != NULL) {
@@ -1279,7 +1306,7 @@ void WiFiManager::processPortalConnect() {
? String(F("WiFi connected. Redirecting to ")) + _cpConnectStationIp
: String(F("WiFi connected"));
if (_disableConfigPortal && (configPortalActive || webPortalActive)) {
_cpConnectCloseAt = now + successHandoffDelayMs;
_cpConnectCloseAt = now + kPortalSuccessHandoffFallbackMs;
_cpConnectState = wm_cp_connect_state_t::success_waiting_close;
} else {
finalizePortalConnectSuccess();
@@ -2635,7 +2662,7 @@ bool WiFiManager::setPortalConfig(const WiFiManagerPortalConfig& config) {
? WiFiManagerPortalText::progmem(kDefaultPortalTitle)
: config.title;
_portalBrand.identityTextOverride = config.identityText;
_portalBrand.homeIntro = config.homeIntro;
_portalBrand.tagline = config.tagline;
_portalBrand.logo = config.logo;
_portalBrand.logoAltText = config.logoAltText;
_portalTheme = config.theme;
+30 -3
View File
@@ -1064,12 +1064,12 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() {
const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
String json;
reservePage(json, 1200);
json += F("{\"contractVersion\":2");
json += F("{\"contractVersion\":3");
json += F(",\"brand\":{");
json += F("\"title\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.title);
json += F("\",\"homeIntro\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.homeIntro);
json += F("\",\"tagline\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.tagline);
json += F("\",\"logoSvg\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.logo.svg);
json += F("\",\"logoAltText\":\"");
@@ -1404,6 +1404,33 @@ void WiFiManagerHandlers::handleApiWifiConnectStatus(AsyncWebServerRequest *requ
sendApiJson(request, 200, buildApiWifiConnectStatusJson());
}
void WiFiManagerHandlers::handleApiWifiConnectComplete(AsyncWebServerRequest *request) {
handleRequest(request);
if (!_wm->didConfigPortalConnectSucceed() || _wm->_cpConnectStationIp.length() == 0) {
sendApiJson(request, 409, F("{\"ok\":false,\"message\":\"WiFi hand-off is not ready\"}"));
return;
}
// The SPA has received the station address and can navigate to it. Keep the
// portal alive briefly so the normal device web server can start cleanly.
sendApiJson(request, 200, F("{\"ok\":true}"));
_wm->acknowledgePortalConnectHandoff();
}
void WiFiManagerHandlers::handleApiPortalTimeoutReset(AsyncWebServerRequest *request) {
handleRequest(request);
if (!_wm->configPortalActive || _wm->_configPortalTimeout == 0) {
sendApiJson(request, 409, F("{\"ok\":false,\"message\":\"Portal timeout is not active\"}"));
return;
}
_wm->_configPortalStart = millis();
String json = F("{\"ok\":true,\"timeoutSecondsRemaining\":");
json += String((_wm->_configPortalTimeout + 999UL) / 1000UL);
json += F("}");
sendApiJson(request, 200, json);
}
String WiFiManagerHandlers::buildApiParamsGetJson() {
String json = F("{\"params\":[");
bool first = true;
@@ -96,6 +96,12 @@ void WiFiManagerServer::registerRoutes() {
server->on(WM_G(R_api_wifi_connect_status), HTTP_GET, [this](AsyncWebServerRequest *request) {
this->_handlers->handleApiWifiConnectStatus(request);
});
server->on(WM_G(R_api_wifi_connect_complete), HTTP_POST, [this](AsyncWebServerRequest *request) {
this->_handlers->handleApiWifiConnectComplete(request);
});
server->on(WM_G(R_api_portal_timeout_reset), HTTP_POST, [this](AsyncWebServerRequest *request) {
this->_handlers->handleApiPortalTimeoutReset(request);
});
server->on(WM_G(R_api_params), HTTP_GET, [this](AsyncWebServerRequest *request) {
this->_handlers->handleApiParamsGet(request);
});