Portal customization API v2, SPA hardening, and test cleanup

- Group portal state into brand/pages/actions/layout structs; portal* API
- Bootstrap JSON: nested brand/context/pages/actions/layout; param kind
- Handlers: PROGMEM default CSS when no override/append; dynamic CSS path
- PortalAppJS: portal timeout countdown; safe WiFi scan DOM; serialized
  scan-status polling; fetch error handling; wm:view-changed on redirects
- CSS: scan list icon alignment; wm-status for timeout
- Tests: remove low-value/no-op tests; trim fragile JS substring checks
This commit is contained in:
2026-05-04 22:03:43 +10:00
parent 93711d981d
commit 7735d7f0cb
22 changed files with 1039 additions and 326 deletions
+117 -34
View File
@@ -200,6 +200,74 @@ class WiFiManagerHandlers;
class WiFiManagerDfteLogger;
class WiFiManagerLogSink;
// ---------------------------------------------------------------------------
// Portal customization (v2 contract: nested JSON; all entry points use portal* API)
// ---------------------------------------------------------------------------
enum class PortalParamsLocation : uint8_t { WiFiPage = 0, SetupPage = 1 };
enum class PortalPasswordPlaceholderMode : uint8_t { Hidden = 0, Masked = 1, Actual = 2 };
enum class PortalFieldVisibility : uint8_t { Hidden = 0, Auto = 1, Always = 2 };
enum class PortalHomeCardKind : uint8_t { Text = 0, Callout = 1, KeyValue = 2 };
struct PortalKeyValueItem {
String key;
String label;
String value;
};
struct PortalInfoSection {
String id;
String title;
std::vector<PortalKeyValueItem> items;
};
struct PortalHomeCard {
String id;
String title;
PortalHomeCardKind kind = PortalHomeCardKind::KeyValue;
String text;
std::vector<PortalKeyValueItem> items;
};
struct PortalBrandState {
String title = "WiFiManager";
String identityTextOverride;
String homeIntro;
String logoSvg;
};
struct PortalPageState {
bool infoVisible = true;
bool updateVisible = true;
bool setupVisible = true;
};
struct PortalActionState {
bool eraseVisible = true;
bool restartVisible = true;
bool exitVisible = true;
bool closeCaptiveVisible = true;
bool backVisible = false;
};
struct PortalLayoutState {
/** When true, custom parameters render on the Wi-Fi page; when false, only on #/setup. */
bool paramsOnWifiPage = true;
};
struct PortalAssetState {
String appendedCss;
String overriddenCss;
String appendedJs;
};
struct PortalStructuredExtrasState {
std::vector<PortalInfoSection> infoSections;
std::vector<PortalHomeCard> homeCards;
};
class WiFiManager
{
public:
@@ -322,7 +390,7 @@ class WiFiManager
bool erase(bool opt);
//adds a custom parameter, returns false on failure
bool addParameter(WiFiManagerParameter *p);
bool portalAddParameter(WiFiManagerParameter *p);
//returns the list of Parameters
WiFiManagerParameter** getParameters();
@@ -421,12 +489,6 @@ class WiFiManager
//if true, always show static dns, esle only show if set via setSTAStaticIPConfig
void setShowDnsFields(boolean alwaysShow);
// toggle showing the saved wifi password in wifi form, could be a security issue.
void setShowPassword(boolean show);
//if false, disable captive portal redirection
void setCaptivePortalEnable(boolean enabled);
//if false, timeout captive portal even if a STA client connected to softAP (false), suggest disabling if captiveportal is open
void setAPClientCheck(boolean enabled);
@@ -449,15 +511,6 @@ class WiFiManager
bool setHostname(const char * hostname);
bool setHostname(String hostname);
// show erase wifi onfig button on info page, true
void setShowInfoErase(boolean enabled);
// show OTA upload button on info page
void setShowInfoUpdate(boolean enabled);
// show Info nav / info view (portal SPA); default true
void setShowInfo(boolean enabled);
// set ap channel
void setWiFiAPChannel(int32_t channel);
@@ -467,18 +520,46 @@ class WiFiManager
// clean connect, always disconnect before connecting
void setCleanConnect(bool enable); // default false
// set the webapp title, default WiFiManager
void setTitle(String title);
// ---- Portal (all customization entry points use the portal* prefix) ----
void portalSetBrandTitle(const String& title);
void portalSetContextIdentityText(const String& identityText);
void portalSetBrandHomeIntro(const String& text);
void portalSetBrandLogoSvg(const String& svgMarkup);
/**
* Where custom parameters (`WiFiManagerParameter`) appear in the portal SPA (one placement only).
*
* - true (default): parameters render on the WiFi route (`#/wifi`) together with SSID/password.
* - false: parameters render only on the Setup route (`#/setup`); they are not duplicated on WiFi.
*
* Bootstrap exposes the same flag as `features.paramsInWifi`.
*/
void setParametersEmbeddedInWifi(boolean embedded);
void portalSetPageInfoVisible(bool visible);
void portalSetPageUpdateVisible(bool visible);
void portalSetPageSetupVisible(bool visible);
void portalSetActionEraseVisible(bool visible);
void portalSetActionRestartVisible(bool visible);
void portalSetActionExitVisible(bool visible);
void portalSetActionCloseCaptiveVisible(bool visible);
void portalSetActionBackVisible(bool visible);
void portalSetLayoutParamsLocation(PortalParamsLocation location);
void portalSetBehaviorCaptivePortalEnabled(bool enabled);
void portalSetBehaviorConnectOnSave(bool enabled);
void portalSetBehaviorExitAllowed(bool allowed);
void portalSetBehaviorConnectTimeoutSeconds(unsigned long seconds);
void portalSetBehaviorPortalTimeoutSeconds(unsigned long seconds);
void portalSetBehaviorAutoReconnect(bool enabled);
void portalSetBehaviorApClientCheck(bool enabled);
void portalSetBehaviorWebClientCheck(bool enabled);
void portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode mode);
void portalSetFieldStaticIpVisibility(PortalFieldVisibility visibility);
void portalSetFieldStaticDnsVisibility(PortalFieldVisibility visibility);
void portalClearParameters();
void portalAddInfoSection(const PortalInfoSection& section);
void portalClearInfoSections();
void portalAddHomeCard(const PortalHomeCard& card);
void portalClearHomeCards();
void portalAppendCss(const String& css);
void portalOverrideCss(const String& css);
void portalAppendJs(const String& js);
// get last connection result, including autoconnect and portal credential-save attempts
uint8_t getLastConxResult();
@@ -706,7 +787,7 @@ class WiFiManager
int _staShowStaticFields = 0; // ternary 1=always show static ip fields, 0=only if set, -1=never(cannot change ips via web!)
int _staShowDns = 0; // ternary 1=always show dns, 0=only if set, -1=never(cannot change dns via web!)
boolean _removeDuplicateAPs = true; // remove dup aps from wifiscan
boolean _showPassword = false; // show or hide saved password on wifi form, might be a security issue!
PortalPasswordPlaceholderMode _portalPasswordPlaceholderMode = PortalPasswordPlaceholderMode::Masked;
boolean _shouldBreakAfterConfig = false; // stop configportal on save failure
boolean _enableCaptivePortal = true; // enable captive portal redirection
boolean _userpersistent = true; // users preffered persistence to restore
@@ -714,15 +795,17 @@ class WiFiManager
boolean _apClientCheck = false; // keep cp alive if ap have station
boolean _webClientCheck = true; // keep cp alive if web have client
boolean _scanDispOptions = false; // show percentage in scans not icons
boolean _paramsInWifi = true; // if false, custom params only on #/setup (see setParametersEmbeddedInWifi)
boolean _showInfo = true; // show Info in portal SPA
boolean _showInfoErase = true; // info page erase button
boolean _showInfoUpdate = true; // info page update button
boolean _showBack = false; // show back button
boolean _enableConfigPortal = true; // FOR autoconnect - start config portal if autoconnect failed
boolean _disableConfigPortal = true; // FOR autoconnect - stop config portal if cp wifi save
String _hostname = ""; // hostname for esp8266 for dhcp, and or MDNS
String _title = "WiFiManager"; // app title - default WiFiManager
// Grouped portal presentation / customization (see portal* setters; JSON in v2 bootstrap)
PortalBrandState _portalBrand;
PortalPageState _portalPages;
PortalActionState _portalActions;
PortalLayoutState _portalLayout;
PortalAssetState _portalAssets;
PortalStructuredExtrasState _portalStructured;
// internal options
@@ -20,8 +20,7 @@
#define WM_TEMPLATE_REGISTRY_CAPACITY 16
#endif
// Portal UI customization is driven by WiFiManager state and helpers such as setTitle(), setShowInfo(),
// setParametersEmbeddedInWifi(), setShowInfoUpdate(), etc., surfaced through bootstrap/API JSON — not by mutating template
// Portal UI customization is driven by WiFiManager `portal*` APIs and JSON (/api/...) — not by mutating template
// placeholder registries.
// -----------------------------------------------------------------------------------------------
@@ -106,11 +105,14 @@ class WiFiManagerHandlers {
void appendPortalJsonStaticFields(String& json, bool& first);
void appendPortalJsonCustomParams(String& json, bool& first);
String composePortalStylesheet() const;
void appendPortalExtraInfoSectionsJson(String& json, bool& first);
void appendPortalExtraHomeCardsJson(String& json, bool& first);
void appendJsonKvItem(String& json, bool& first, const char* key, const String& label, const String& value);
void appendOneInfoItemForId(String& json, bool& first, const char* id);
void appendInfoSectionFromIds(String& json, const char* const* ids, size_t count, bool& first);
void appendPortalUiFeatureFlagsJson(String& json);
void appendApiInfoActionsJson(String& json);
static void sendApiJson(AsyncWebServerRequest *request, int code, const String& json);
+9 -3
View File
@@ -70,6 +70,9 @@ const char CSS_STYLE[] PROGMEM = "<style>"
".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}"
".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}"
@@ -85,7 +88,7 @@ const char CSS_STYLE[] PROGMEM = "<style>"
"background:rgba(14,165,233,.06);margin-bottom:16px}"
"body.invert .wm-callout{background:rgba(56,189,248,.08)}"
".wm-callout--info{border-left:4px solid var(--wm-brand)}"
".wm-status{margin:0;font-size:.9rem}"
".wm-status{margin:0 0 10px;font-size:.9rem;font-weight:600;color:var(--wm-brand)}"
/* Form fields */
".wm-field{margin-bottom:14px;text-align:left}"
".wm-field:last-child{margin-bottom:0}"
@@ -130,10 +133,13 @@ const char CSS_STYLE[] PROGMEM = "<style>"
".wm-scan-row:hover{background:rgba(14,165,233,.1)}"
"body.invert .wm-scan-row:hover{background:rgba(56,189,248,.12)}"
".wm-scan-ssid{flex:1;min-width:0;font-weight:600;font-size:.95rem;text-align:left;word-break:break-word}"
".wm-scan-signal{display:flex;align-items:center;gap:6px;flex-shrink:0;font-size:.8rem;color:var(--wm-muted);font-weight:600}"
".wm-scan-meta{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-shrink:0;min-width:78px}"
".wm-scan-signal{display:block;flex-shrink:0;font-size:.8rem;color:var(--wm-muted);font-weight:600;min-width:38px;text-align:right}"
".wm-scan-lock,.wm-scan-bars{display:block;flex:0 0 16px}"
// quality icons
".q{height:16px;width:16px;margin:0;padding:0;flex-shrink:0}.q.q-0:after{background-position-x:0}.q.q-1:after{background-position-x:-16px}.q.q-2:after{background-position-x:-32px}.q.q-3:after{background-position-x:-48px}.q.q-4:after{background-position-x:-64px}.q.l:before{background-position-x:-80px;padding-right:5px}.ql .q{float:left}.q:after,.q:before{content:'';width:16px;height:16px;display:inline-block;background-repeat:no-repeat;background-position:16px 0;"
".q{position:relative;height:16px;width:16px;margin:0;padding:0;flex-shrink:0}.q.q-0:after{background-position-x:0}.q.q-1:after{background-position-x:-16px}.q.q-2:after{background-position-x:-32px}.q.q-3:after{background-position-x:-48px}.q.q-4:after{background-position-x:-64px}.q.l:before{background-position-x:-80px}.ql .q{float:left}.q:after,.q:before{content:'';position:absolute;left:0;top:0;width:16px;height:16px;display:block;background-repeat:no-repeat;background-position:16px 0;"
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAQCAMAAADeZIrLAAAAJFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHJj5lAAAAC3RSTlMAIjN3iJmqu8zd7vF8pzcAAABsSURBVHja7Y1BCsAwCASNSVo3/v+/BUEiXnIoXkoX5jAQMxTHzK9cVSnvDxwD8bFx8PhZ9q8FmghXBhqA1faxk92PsxvRc2CCCFdhQCbRkLoAQ3q/wWUBqG35ZxtVzW4Ed6LngPyBU2CobdIDQ5oPWI5nCUwAAAAASUVORK5CYII=');}"
".wm-scan-lock:after,.wm-scan-bars:before{content:none}"
"@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.q:before,.q:after {"
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAAgCAMAAACfM+KhAAAALVBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAOrOgAAAADnRSTlMAESIzRGZ3iJmqu8zd7gKjCLQAAACmSURBVHgB7dDBCoMwEEXRmKlVY3L//3NLhyzqIqSUggy8uxnhCR5Mo8xLt+14aZ7wwgsvvPA/ofv9+44334UXXngvb6XsFhO/VoC2RsSv9J7x8BnYLW+AjT56ud/uePMdb7IP8Bsc/e7h8Cfk912ghsNXWPpDC4hvN+D1560A1QPORyh84VKLjjdvfPFm++i9EWq0348XXnjhhT+4dIbCW+WjZim9AKk4UZMnnCEuAAAAAElFTkSuQmCC');"
"background-size: 95px 16px;}}"
+192 -40
View File
@@ -24,6 +24,7 @@ function esc(t){if(!t)return'';return String(t).replace(/[&<>"']/g,function(c){r
// --- Toast / dialog ---
var _wmToastTimer=null;
var _wmPortalTimeoutTimer=null;
function showToast(msg,isErr){
var el=$('wm-toast');
if(!el)return;
@@ -72,9 +73,17 @@ function bindFormSubmitHandlers(){
}
// --- View shell ---
function stopPortalTimeoutCountdown(){
if(_wmPortalTimeoutTimer){
clearInterval(_wmPortalTimeoutTimer);
_wmPortalTimeoutTimer=null;
}
}
function setView(html){
var a=$('app');
if(!a)return;
stopPortalTimeoutCountdown();
stopWifiScanPolling();
stopWifiConnectPolling();
a.innerHTML="<div class='wm-layout'>"+html+"</div>";
@@ -82,10 +91,35 @@ function setView(html){
bindFormSubmitHandlers();
}
function formatDurationSeconds(totalSeconds){
totalSeconds=Math.max(0,Math.floor(totalSeconds||0));
var hours=Math.floor(totalSeconds/3600);
var minutes=Math.floor((totalSeconds%3600)/60);
var seconds=totalSeconds%60;
if(hours>0){
return hours+":"+String(minutes).padStart(2,'0')+":"+String(seconds).padStart(2,'0');
}
return minutes+":"+String(seconds).padStart(2,'0');
}
function startPortalTimeoutCountdown(initialSeconds){
var el=$('wm-portal-timeout');
var remaining=Math.max(0,Math.floor(initialSeconds||0));
if(!el||remaining<=0)return;
function render(){
if(!el)return;
el.textContent='Captive portal timeout: '+formatDurationSeconds(remaining);
}
render();
_wmPortalTimeoutTimer=setInterval(function(){
remaining=Math.max(0,remaining-1);
render();
if(remaining<=0)stopPortalTimeoutCountdown();
},1000);
}
// --- Field render (WiFi / params meta) ---
function renderMetaField(f){
if(!f)return'';
if(f.html)return f.html;
function renderStandardField(f){
var id=f.id||f.name||'';
var nm=f.name||f.id||'';
var domId=(id==='s')?'wm-s':(id==='p')?'wm-p':('wm-f-'+id);
@@ -102,6 +136,13 @@ function renderMetaField(f){
return h;
}
function renderMetaField(f){
if(!f)return'';
if(f.kind==='html')return f.html||'';
if(f.kind==='field' || !f.kind)return renderStandardField(f);
return '';
}
function renderFieldList(arr){
if(!arr||!arr.length)return'';
var h='',i;
@@ -111,43 +152,78 @@ function renderFieldList(arr){
function navBar(active){
active=active||'home';
var f=boot.features||{};
var pages=boot.pages||{};
var layout=boot.layout||{};
var actions=boot.actions||{};
var pinWifi=(layout.paramsLocation||'wifi')==='wifi';
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='#/'>Home</a>";
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>WiFi</a>";
if(!f.paramsInWifi){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>Setup</a>";}
if(f.showInfo!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>Info</a>";}
if(f.showUpdate){h+="<a class='wm-nav-link"+(active==='update'?' wm-nav-link--active':'')+"' href='#/update'>Update</a>";}
if(boot.showBack){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>Back</a>";}
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>Setup</a>";}
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>Info</a>";}
if(pu.visible){h+="<a class='wm-nav-link"+(active==='update'?' wm-nav-link--active':'')+"' href='#/update'>Update</a>";}
if(actions.back&&actions.back.visible){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>Back</a>";}
h+="</nav>";
return h;
}
function deviceActionsHtml(){
var f=boot.features||{};
var actions=boot.actions||{};
var h='', ok=false;
if(f.showRestart){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>Restart device</button>";ok=true;}
if(f.showExitPortal){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>Exit portal</button>";ok=true;}
if(f.showCloseCaptive){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>Stop captive portal</button>";ok=true;}
if(actions.restart&&actions.restart.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>Restart device</button>";ok=true;}
if(actions.exitPortal&&actions.exitPortal.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>Exit portal</button>";ok=true;}
if(actions.closeCaptive&&actions.closeCaptive.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>Stop captive portal</button>";ok=true;}
if(!ok)return'';
return "<div class='wm-card'><h2 class='wm-card-title'>Device</h2><div class='wm-btn-group'>"+h+"</div></div>";
}
function renderExtraHomeCards(cards){
if(!cards||!cards.length)return'';
var out='',i,c,j,k,cls;
for(i=0;i<cards.length;i++){
c=cards[i];
if(!c)continue;
out+="<div class='wm-card'>";
if(c.title){out+="<h2 class='wm-card-title'>"+esc(c.title)+"</h2>";}
if(c.kind==='text'||c.kind==='callout'){
cls=c.kind==='callout'?'wm-callout wm-callout--info':'wm-lead';
out+="<p class='"+cls+"'>"+esc(c.text||'')+"</p>";
}else{
out+="<dl class='wm-kv'>";
if(c.items){for(j=0;j<c.items.length;j++){k=c.items[j];out+="<dt>"+esc(k.label)+"</dt><dd>"+esc(k.value)+"</dd>";}}
out+="</dl>";
}
out+="</div>";
}
return out;
}
function viewHome(){
var sub=esc(boot.subtitle||'');
var t=esc(boot.title||'WiFiManager');
var brand=boot.brand||{};
var ctx=boot.context||{};
var t=esc(brand.title||'WiFiManager');
var ident=ctx.identityText?esc(ctx.identityText):'';
var summary=ctx.statusSummary||'';
var timeoutSeconds=ctx.portalTimeoutSecondsRemaining||0;
var html=navBar('home');
html+="<header class='wm-hero'><h1 class='wm-hero-title'>"+t+"</h1>";
if(sub)html+="<p class='wm-hero-sub'>"+sub+"</p>";
html+="<header class='wm-hero'>";
if(brand.logoSvg){html+="<div class='wm-brand-logo'>"+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'><h2 class='wm-card-title'>Status</h2>";
if(boot.initialStatus){
html+="<p class='wm-home-summary'>"+esc(boot.initialStatus)+"</p>";
}
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+=renderExtraHomeCards(boot.extraHomeCards||[]);
html+=deviceActionsHtml();
setView(html);
if(timeoutSeconds>0)startPortalTimeoutCountdown(timeoutSeconds);
}
// --- HTTP ---
@@ -159,24 +235,50 @@ function api(path, opt){
// --- WiFi scan view ---
var _wmWifiScanPollTimer=null;
var _wmWifiScanPollToken=0;
function getScanResultsBox(){
return $('wm-scan-results');
}
function setScanRefreshEnabled(enabled){
var btn=$('wm-refresh-scan');
if(btn)btn.disabled=(enabled===false);
}
function stopWifiScanPolling(){
_wmWifiScanPollToken++;
if(_wmWifiScanPollTimer){
clearInterval(_wmWifiScanPollTimer);
clearTimeout(_wmWifiScanPollTimer);
_wmWifiScanPollTimer=null;
}
setScanRefreshEnabled(true);
}
function startWifiScanPolling(box){
stopWifiScanPolling();
_wmWifiScanPollTimer=setInterval(function(){
setScanRefreshEnabled(false);
var token=++_wmWifiScanPollToken;
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;}
box=getScanResultsBox()||box;
if(box)box.innerHTML=renderScanList(d);
if(!d.scanning)stopWifiScanPolling();
if(!d.scanning){
stopWifiScanPolling();
return;
}
_wmWifiScanPollTimer=setTimeout(poll,800);
}).catch(function(){
if(token!==_wmWifiScanPollToken)return;
stopWifiScanPolling();
box=getScanResultsBox()||box;
if(box)box.innerHTML=renderScanError('Portal became unreachable while scanning.');
});
},800);
}
poll();
}
function renderScanList(data){
@@ -197,23 +299,42 @@ function renderScanList(data){
n=data.networks[i];
pct=(n.quality||0)+'%';
qi=Math.round((n.quality/100)*3)+1;if(qi<1)qi=1;if(qi>4)qi=4;
enc=n.encrypted?'l':'';
html+="<a class='wm-scan-row' href='#p' data-ssid='"+esc(n.ssid)+"'>";
html+="<span class='wm-scan-ssid'>"+esc(n.ssid)+"</span>";
html+="<span role='img' aria-label='"+pct+"' title='"+pct+"' class='q q-"+qi+" "+enc+"'></span>";
html+="<span class='wm-scan-meta'>";
if(n.encrypted){html+="<span class='wm-scan-lock q l' aria-hidden='true'></span>";}
html+="<span role='img' aria-label='"+pct+"' title='"+pct+"' class='wm-scan-bars q q-"+qi+"'></span>";
html+="<span class='wm-scan-signal'>"+pct+"</span>";
html+="</span>";
html+="</a>";
}
html+="</div>";
return html;
}
function renderScanError(msg){
return "<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>"+esc(msg||'Failed to reach portal while scanning.')+"</p></div>";
}
function wifiRefresh(){
var box=document.getElementById('wm-scan-results');
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>";
api('/api/wifi/scan',{method:'POST'}).then(function(){
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);
return;
}
startWifiScanPolling(box);
}).catch(function(){
stopWifiScanPolling();
box=getScanResultsBox()||box;
if(box)box.innerHTML=renderScanError('Failed to reach portal while starting scan.');
showToast('Failed to reach portal while starting scan.',true);
});
}
@@ -273,12 +394,19 @@ function viewWifi(){
html+="<div id='wm-wifi-msg'></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={};}
var box=document.getElementById('wm-scan-results');
var box=getScanResultsBox();
if(box)box.innerHTML=renderScanList(d);
if(d&&d.scanning){startWifiScanPolling(box);}
}).catch(function(){
var box=getScanResultsBox();
if(box)box.innerHTML=renderScanError('Failed to reach portal while loading scan status.');
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>");
});
}
@@ -310,6 +438,10 @@ function pollWifiConnectStatus(){
stopWifiConnectPolling();
showToast(j.message||'WiFi connect failed',true);
}
}).catch(function(){
stopWifiConnectPolling();
var msg=$('wm-wifi-msg');
if(msg)msg.innerHTML='Portal became unreachable while checking connection status.';
});
},700);
}
@@ -363,6 +495,15 @@ function viewInfo(){
html+=section('Device',d.device||[]);
html+=section('WiFi',d.wifi||[]);
html+=section('About',d.about||[]);
if(d.extraSections){
var xi,zs,zrows,zi,it;
for(xi=0;xi<d.extraSections.length;xi++){
zs=d.extraSections[xi];
zrows=[];
if(zs.items){for(zi=0;zi<zs.items.length;zi++){it=zs.items[zi];zrows.push({label:it.label,value:esc(it.value)});}}
html+=section(zs.title,zrows);
}
}
var act=d.actions||{};
html+="<div class='wm-card'><h2 class='wm-card-title'>Actions</h2><div class='wm-btn-group'>";
if(act.showRestart)html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>Restart device</button>";
@@ -426,10 +567,11 @@ window.portalParamSave=function(ev){
};
function viewUpdate(){
var f=boot.features||{};
var pages=boot.pages||{};
var pupd=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(!f.showUpdate){
if(!pupd.visible){
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this configuration.</p></div>";
setView(html);
return;
@@ -484,19 +626,29 @@ window.portalOtaSubmit=function(ev){
function route(){
var h=location.hash||'#/';
if(h.indexOf('#/')!==0)h='#/';
var f=boot.features||{};
if(h==='#/info'&&f.showInfo===false){viewHome();return;}
if(h==='#/update'&&f.showUpdate===false){viewHome();return;}
if(h==='#/'||h==='#'){viewHome();return;}
if(h==='#/wifi'){viewWifi();return;}
if(h==='#/info'){viewInfo();return;}
if(h==='#/setup'){viewSetup();return;}
if(h==='#/update'){viewUpdate();return;}
viewHome();
var pages=boot.pages||{};
var ps=pages.setup||{};
var pi=pages.info||{};
var pu=pages.update||{};
if(h==='#/setup'&&ps.visible===false){viewHome();try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:'#/'}}));}catch(e){}return;}
if(h==='#/info'&&pi.visible===false){viewHome();try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:'#/'}}));}catch(e){}return;}
if(h==='#/update'&&pu.visible===false){viewHome();try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:'#/'}}));}catch(e){}return;}
if(h==='#/'||h==='#'){viewHome();}
else if(h==='#/wifi'){viewWifi();}
else if(h==='#/info'){viewInfo();}
else if(h==='#/setup'){viewSetup();}
else if(h==='#/update'){viewUpdate();}
else {viewHome();h='#/';}
try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:h}}));}catch(e){}
}
function bootPortal(){
route();
try{document.dispatchEvent(new CustomEvent('wm:ready',{detail:{boot:boot}}));}catch(e){}
}
window.addEventListener('hashchange',route);
window.addEventListener('load',route);
window.addEventListener('load',bootPortal);
})();
)rawliteral";
@@ -7,9 +7,8 @@
* Single-page portal shell: only HTML document served for GET /.
* Dynamic UI is driven by embedded JS + JSON APIs under /api/...
*
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%) are filled per request in
* WiFiManagerHandlers::handleRoot (shell contract comment in WiFiManagerHandlers.cpp); do not treat
* placeholders as a customization API.
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%, %PORTAL_APPEND_JS%) are filled
* per request in WiFiManagerHandlers::handleRoot; do not treat placeholders as a customization API.
*/
#ifndef _WM_ROOT_SHELL_TEMPLATE_H_
@@ -42,6 +41,9 @@ const char WM_ROOT_SHELL_TEMPLATE[] PROGMEM =
"<script>"
"%PORTAL_APP_JS%"
"</script>"
"<script>"
"%PORTAL_APPEND_JS%"
"</script>"
"</body>"
"</html>";
+148 -50
View File
@@ -62,7 +62,7 @@ bool normalizeHostname(String& hostname) {
* @param p Pointer to WiFiManagerParameter to add
* @return true if added successfully, false on error
*/
bool WiFiManager::addParameter(WiFiManagerParameter *p) {
bool WiFiManager::portalAddParameter(WiFiManagerParameter *p) {
// check param id is valid, unless null
if(p->getID()){
@@ -111,6 +111,15 @@ bool WiFiManager::addParameter(WiFiManagerParameter *p) {
return true;
}
void WiFiManager::portalClearParameters() {
if (_params != NULL) {
free(_params);
_params = NULL;
}
_paramsCount = 0;
_max_params = WIFI_MANAGER_MAX_PARAMS;
}
WiFiManagerParameter** WiFiManager::getParameters() {
return _params;
}
@@ -1927,29 +1936,6 @@ void WiFiManager::setShowDnsFields(boolean alwaysShow){
else _staShowDns = alwaysShow ? 1 : 0;
}
/**
* toggle showing password in wifi password field
* if not enabled, placeholder will be S_passph
* @since $dev
* @access public
* @param boolean alwaysShow [false]
*/
void WiFiManager::setShowPassword(boolean show){
_showPassword = show;
}
/**
* toggle captive portal
* if enabled, then devices that use captive portal checks will be redirected to root
* if not you will automatically have to navigate to ip [192.168.4.1]
* @since $dev
* @access public
* @param boolean enabled [true]
*/
void WiFiManager::setCaptivePortalEnable(boolean enabled){
_enableCaptivePortal = enabled;
}
/**
* toggle wifi autoreconnect policy
* if enabled, then wifi will autoreconnect automatically always
@@ -2076,24 +2062,148 @@ void WiFiManager::setWiFiAPHidden(bool hidden){
}
/**
* toggle showing erase wifi config button on info page
* @param boolean enabled
*/
void WiFiManager::setShowInfoErase(boolean enabled){
_showInfoErase = enabled;
void WiFiManager::portalSetBrandTitle(const String& title) {
_portalBrand.title = title;
}
/**
* toggle showing update upload web ota button on info page
* @param boolean enabled
*/
void WiFiManager::setShowInfoUpdate(boolean enabled){
_showInfoUpdate = enabled;
void WiFiManager::portalSetContextIdentityText(const String& identityText) {
_portalBrand.identityTextOverride = identityText;
}
void WiFiManager::setShowInfo(boolean enabled){
_showInfo = enabled;
void WiFiManager::portalSetBrandHomeIntro(const String& text) {
_portalBrand.homeIntro = text;
}
void WiFiManager::portalSetBrandLogoSvg(const String& svgMarkup) {
_portalBrand.logoSvg = svgMarkup;
}
void WiFiManager::portalSetPageInfoVisible(bool visible) {
_portalPages.infoVisible = visible;
}
void WiFiManager::portalSetPageUpdateVisible(bool visible) {
_portalPages.updateVisible = visible;
}
void WiFiManager::portalSetPageSetupVisible(bool visible) {
_portalPages.setupVisible = visible;
}
void WiFiManager::portalSetActionEraseVisible(bool visible) {
_portalActions.eraseVisible = visible;
}
void WiFiManager::portalSetActionRestartVisible(bool visible) {
_portalActions.restartVisible = visible;
}
void WiFiManager::portalSetActionExitVisible(bool visible) {
_portalActions.exitVisible = visible;
}
void WiFiManager::portalSetActionCloseCaptiveVisible(bool visible) {
_portalActions.closeCaptiveVisible = visible;
}
void WiFiManager::portalSetActionBackVisible(bool visible) {
_portalActions.backVisible = visible;
}
void WiFiManager::portalSetLayoutParamsLocation(PortalParamsLocation location) {
_portalLayout.paramsOnWifiPage = (location == PortalParamsLocation::WiFiPage);
}
void WiFiManager::portalSetBehaviorCaptivePortalEnabled(bool enabled) {
_enableCaptivePortal = enabled;
}
void WiFiManager::portalSetBehaviorConnectOnSave(bool enabled) {
setSaveConnect(enabled);
}
void WiFiManager::portalSetBehaviorExitAllowed(bool allowed) {
_allowExit = allowed;
}
void WiFiManager::portalSetBehaviorConnectTimeoutSeconds(unsigned long seconds) {
setConnectTimeout(seconds);
}
void WiFiManager::portalSetBehaviorPortalTimeoutSeconds(unsigned long seconds) {
setConfigPortalTimeout(seconds);
}
void WiFiManager::portalSetBehaviorAutoReconnect(bool enabled) {
setWiFiAutoReconnect(enabled);
}
void WiFiManager::portalSetBehaviorApClientCheck(bool enabled) {
setAPClientCheck(enabled);
}
void WiFiManager::portalSetBehaviorWebClientCheck(bool enabled) {
setWebPortalClientCheck(enabled);
}
void WiFiManager::portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode mode) {
_portalPasswordPlaceholderMode = mode;
}
void WiFiManager::portalSetFieldStaticIpVisibility(PortalFieldVisibility visibility) {
switch (visibility) {
case PortalFieldVisibility::Always:
setShowStaticFields(true);
break;
case PortalFieldVisibility::Auto:
setShowStaticFields(false);
break;
case PortalFieldVisibility::Hidden:
_staShowStaticFields = -1;
break;
}
}
void WiFiManager::portalSetFieldStaticDnsVisibility(PortalFieldVisibility visibility) {
switch (visibility) {
case PortalFieldVisibility::Always:
setShowDnsFields(true);
break;
case PortalFieldVisibility::Auto:
setShowDnsFields(false);
break;
case PortalFieldVisibility::Hidden:
_staShowDns = -1;
break;
}
}
void WiFiManager::portalAddInfoSection(const PortalInfoSection& section) {
_portalStructured.infoSections.push_back(section);
}
void WiFiManager::portalClearInfoSections() {
_portalStructured.infoSections.clear();
}
void WiFiManager::portalAddHomeCard(const PortalHomeCard& card) {
_portalStructured.homeCards.push_back(card);
}
void WiFiManager::portalClearHomeCards() {
_portalStructured.homeCards.clear();
}
void WiFiManager::portalAppendCss(const String& css) {
_portalAssets.appendedCss += css;
}
void WiFiManager::portalOverrideCss(const String& css) {
_portalAssets.overriddenCss = css;
}
void WiFiManager::portalAppendJs(const String& js) {
_portalAssets.appendedJs += js;
}
/**
@@ -2174,18 +2284,6 @@ DNSServer* WiFiManager::getDNSServer() {
return _serverManager ? _serverManager->getDNSServer() : nullptr;
}
/**
* [setTitle description]
* @param String title, set app title
*/
void WiFiManager::setTitle(String title){
_title = title;
}
void WiFiManager::setParametersEmbeddedInWifi(boolean embedded) {
_paramsInWifi = embedded;
}
// GETTERS
/**
+251 -81
View File
@@ -13,7 +13,6 @@
#include "templates/PortalAppJS.h"
#include <TemplateEngine.h>
#include <cstring>
#if defined(ESP8266) || defined(ESP32)
#ifndef WM_PAGE_RESERVE_BYTES
@@ -54,12 +53,42 @@ size_t dynamicStringLengthGetter(const char* data, void* /*userData*/) {
return data ? strlen(data) : 0;
}
void configureDynamicStringDescriptor(DynamicTemplateDescriptor& descriptor, String& value) {
void configureDynamicStringDescriptor(DynamicDataDescriptor& descriptor, String& value) {
descriptor.getter = &dynamicStringGetter;
descriptor.getLength = &dynamicStringLengthGetter;
descriptor.userData = &value;
}
String readProgmemString(const char* p) {
return String(FPSTR(p));
}
String stripStyleWrapper(const String& styleBlock) {
String css = styleBlock;
const String openTag = F("<style>");
const String closeTag = F("</style>");
if (css.startsWith(openTag)) {
css.remove(0, openTag.length());
}
if (css.endsWith(closeTag)) {
css.remove(css.length() - closeTag.length());
}
return css;
}
const char* portalHomeCardKindJson(PortalHomeCardKind k) {
switch (k) {
case PortalHomeCardKind::Text:
return "text";
case PortalHomeCardKind::Callout:
return "callout";
case PortalHomeCardKind::KeyValue:
default:
return "kv";
}
}
template <typename BundleT>
AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
const std::shared_ptr<BundleT>& bundle,
@@ -73,41 +102,123 @@ AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
);
}
// Shell contract (must match templates/RootShell.h — customize UI via WiFiManager APIs + bootstrap JSON, not registries):
// %PAGE_TITLE% -> document title (from WiFiManager title state)
// %STYLES% -> embedded portal CSS
// %BOOTSTRAP_JSON% -> initial SPA runtime payload
// %PORTAL_APP_JS% -> embedded SPA source
// Shell contract (must match templates/RootShell.h — customize UI via WiFiManager portal* APIs + bootstrap JSON, not registries):
// %PAGE_TITLE% -> document title (from WiFiManager title state)
// %STYLES% -> portal CSS (embedded default + portalAppendCss / portalOverrideCss)
// %BOOTSTRAP_JSON% -> initial SPA runtime payload
// %PORTAL_APP_JS% -> embedded SPA source
// %PORTAL_APPEND_JS% -> consumer hook script (portalAppendJs)
struct PortalShellRenderBundle {
String bootstrapJson;
String pageTitleStatic;
String stylesStatic;
String appendJsStatic;
PlaceholderRegistry registry;
TemplateContext context;
DynamicTemplateDescriptor bootstrapDescriptor;
DynamicTemplateDescriptor pageTitleDescriptor;
DynamicDataDescriptor bootstrapDescriptor;
DynamicDataDescriptor pageTitleDescriptor;
DynamicDataDescriptor stylesDescriptor;
DynamicDataDescriptor appendJsDescriptor;
PortalShellRenderBundle()
: registry(WM_TEMPLATE_REGISTRY_CAPACITY), bootstrapDescriptor{}, pageTitleDescriptor{} {}
: registry(WM_TEMPLATE_REGISTRY_CAPACITY),
bootstrapDescriptor{},
pageTitleDescriptor{},
stylesDescriptor{},
appendJsDescriptor{} {}
};
void populatePortalShellBundle(PortalShellRenderBundle& bundle,
const String& pageTitle,
const String& bootstrapJson) {
bundle.pageTitleStatic = pageTitle;
bundle.bootstrapJson = bootstrapJson;
bundle.registry.registerProgmemData("%STYLES%", CSS_STYLE);
bundle.registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
configureDynamicStringDescriptor(bundle.pageTitleDescriptor, bundle.pageTitleStatic);
bundle.registry.registerDynamicTemplate("%PAGE_TITLE%", &bundle.pageTitleDescriptor);
configureDynamicStringDescriptor(bundle.bootstrapDescriptor, bundle.bootstrapJson);
bundle.registry.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bundle.bootstrapDescriptor);
}
} // namespace
WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {}
String WiFiManagerHandlers::composePortalStylesheet() const {
String css;
if (_wm != nullptr && _wm->_portalAssets.overriddenCss.length() > 0) {
css = _wm->_portalAssets.overriddenCss;
} else {
css = stripStyleWrapper(readProgmemString(CSS_STYLE));
}
if (_wm != nullptr && _wm->_portalAssets.appendedCss.length() > 0) {
css += _wm->_portalAssets.appendedCss;
}
String out = F("<style>");
out += css;
out += F("</style>");
return out;
}
void WiFiManagerHandlers::appendPortalExtraInfoSectionsJson(String& json, bool& first) {
if (_wm == nullptr) {
return;
}
for (const auto& sec : _wm->_portalStructured.infoSections) {
if (!first) {
json += F(",");
}
first = false;
json += F("{\"id\":\"");
jsonAppendEscaped(json, sec.id);
json += F("\",\"title\":\"");
jsonAppendEscaped(json, sec.title);
json += F("\",\"items\":[");
bool firstItem = true;
for (const auto& it : sec.items) {
if (!firstItem) {
json += F(",");
}
firstItem = false;
json += F("{\"key\":\"");
jsonAppendEscaped(json, it.key);
json += F("\",\"label\":\"");
jsonAppendEscaped(json, it.label);
json += F("\",\"value\":\"");
jsonAppendEscaped(json, it.value);
json += F("\"}");
}
json += F("]}");
}
}
void WiFiManagerHandlers::appendPortalExtraHomeCardsJson(String& json, bool& first) {
if (_wm == nullptr) {
return;
}
for (const auto& card : _wm->_portalStructured.homeCards) {
if (!first) {
json += F(",");
}
first = false;
json += F("{\"id\":\"");
jsonAppendEscaped(json, card.id);
json += F("\",\"title\":\"");
jsonAppendEscaped(json, card.title);
json += F("\",\"kind\":\"");
json += portalHomeCardKindJson(card.kind);
json += F("\",\"text\":\"");
jsonAppendEscaped(json, card.text);
json += F("\",\"items\":[");
bool firstItem = true;
for (const auto& it : card.items) {
if (!firstItem) {
json += F(",");
}
firstItem = false;
json += F("{\"key\":\"");
jsonAppendEscaped(json, it.key);
json += F("\",\"label\":\"");
jsonAppendEscaped(json, it.label);
json += F("\",\"value\":\"");
jsonAppendEscaped(json, it.value);
json += F("\"}");
}
json += F("]}");
}
}
void WiFiManagerHandlers::collectVisibleScanResults(std::vector<const WiFiManager::WiFiScanNetwork*>& networks) {
networks.clear();
networks.reserve(_wm->_scanResultsCache.size());
@@ -197,7 +308,7 @@ void WiFiManagerHandlers::appendPortalJsonCustomParams(String& json, bool& first
first = false;
if (p->getID() != nullptr) {
String pid = String(p->getID());
json += F("{\"name\":\"");
json += F("{\"kind\":\"field\",\"name\":\"");
jsonAppendEscaped(json, pid);
json += F("\",\"id\":\"");
jsonAppendEscaped(json, pid);
@@ -219,7 +330,7 @@ void WiFiManagerHandlers::appendPortalJsonCustomParams(String& json, bool& first
}
json += F("}");
} else {
json += F("{\"html\":\"");
json += F("{\"kind\":\"html\",\"html\":\"");
jsonAppendEscaped(json, String(p->getCustomHTML()));
json += F("}");
}
@@ -560,9 +671,25 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) {
// - Populate one request-scoped PlaceholderRegistry with shell defaults + request payloads.
// - Render WM_ROOT_SHELL_TEMPLATE.
auto bundle = std::make_shared<PortalShellRenderBundle>();
populatePortalShellBundle(*bundle,
_wm ? _wm->_title : String(),
buildPortalBootstrapJson());
const bool hasCustomStyles = _wm != nullptr &&
(_wm->_portalAssets.overriddenCss.length() > 0 || _wm->_portalAssets.appendedCss.length() > 0);
bundle->pageTitleStatic = _wm ? _wm->_portalBrand.title : String();
bundle->bootstrapJson = buildPortalBootstrapJson();
bundle->appendJsStatic = _wm ? _wm->_portalAssets.appendedJs : String();
bundle->registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
if (hasCustomStyles) {
bundle->stylesStatic = composePortalStylesheet();
configureDynamicStringDescriptor(bundle->stylesDescriptor, bundle->stylesStatic);
bundle->registry.registerDynamicData("%STYLES%", &bundle->stylesDescriptor);
} else {
bundle->registry.registerProgmemData("%STYLES%", CSS_STYLE);
}
configureDynamicStringDescriptor(bundle->appendJsDescriptor, bundle->appendJsStatic);
bundle->registry.registerDynamicData("%PORTAL_APPEND_JS%", &bundle->appendJsDescriptor);
configureDynamicStringDescriptor(bundle->pageTitleDescriptor, bundle->pageTitleStatic);
bundle->registry.registerDynamicData("%PAGE_TITLE%", &bundle->pageTitleDescriptor);
configureDynamicStringDescriptor(bundle->bootstrapDescriptor, bundle->bootstrapJson);
bundle->registry.registerDynamicData("%BOOTSTRAP_JSON%", &bundle->bootstrapDescriptor);
request->send(beginTemplateResponse(request, bundle, WM_ROOT_SHELL_TEMPLATE));
}
@@ -634,7 +761,7 @@ void WiFiManagerHandlers::applyWifiAndParamsFromRequest(AsyncWebServerRequest *r
_wm->_presavewificallback();
}
if (_wm->_paramsInWifi) {
if (_wm->_portalLayout.paramsOnWifiPage) {
doParamSave(requestArgs);
}
}
@@ -821,38 +948,20 @@ void WiFiManagerHandlers::sendApiJson(AsyncWebServerRequest *request, int code,
request->send(response);
}
void WiFiManagerHandlers::appendPortalUiFeatureFlagsJson(String& json) {
const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
json += F("\"showInfo\":");
json += _wm->_showInfo ? F("true") : F("false");
json += F(",\"showUpdate\":");
json += _wm->_showInfoUpdate ? F("true") : F("false");
json += F(",\"showErase\":");
json += _wm->_showInfoErase ? F("true") : F("false");
json += F(",\"paramsInWifi\":");
json += _wm->_paramsInWifi ? F("true") : F("false");
json += F(",\"showRestart\":");
json += portalRunning ? F("true") : F("false");
json += F(",\"showExitPortal\":");
json += (_wm->_allowExit && portalRunning) ? F("true") : F("false");
json += F(",\"showCloseCaptive\":");
json += (_wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false");
}
void WiFiManagerHandlers::appendApiInfoActionsJson(String& json) {
const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
json += F("\"showUpdate\":");
json += _wm->_showInfoUpdate ? F("true") : F("false");
json += _wm->_portalPages.updateVisible ? F("true") : F("false");
json += F(",\"showErase\":");
json += _wm->_showInfoErase ? F("true") : F("false");
json += _wm->_portalActions.eraseVisible ? F("true") : F("false");
json += F(",\"showBack\":");
json += _wm->_showBack ? F("true") : F("false");
json += _wm->_portalActions.backVisible ? F("true") : F("false");
json += F(",\"showRestart\":");
json += portalRunning ? F("true") : F("false");
json += (_wm->_portalActions.restartVisible && portalRunning) ? F("true") : F("false");
json += F(",\"showExitPortal\":");
json += (_wm->_allowExit && portalRunning) ? F("true") : F("false");
json += (_wm->_portalActions.exitVisible && _wm->_allowExit && portalRunning) ? F("true") : F("false");
json += F(",\"showCloseCaptive\":");
json += (_wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false");
json += (_wm->_portalActions.closeCaptiveVisible && _wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false");
}
String WiFiManagerHandlers::buildPortalBootstrapJson() {
@@ -882,38 +991,87 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() {
stateStr = "idle";
break;
}
const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
String json;
reservePage(json, 512);
json += F("{");
reservePage(json, 1200);
json += F("{\"contractVersion\":2");
json += F(",\"brand\":{");
json += F("\"title\":\"");
jsonAppendEscaped(json, _wm->_title);
json += F("\",\"subtitle\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.title);
json += F("\",\"homeIntro\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.homeIntro);
json += F("\",\"logoSvg\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.logoSvg);
json += F("\"}");
json += F(",\"context\":{");
json += F("\"portalActive\":");
json += portalRunning ? F("true") : F("false");
json += F(",\"portalTimeoutSecondsRemaining\":");
{
String sub;
if (_wm->configPortalActive) {
sub = _wm->_apName;
} else {
sub = _wm->getWiFiHostname() + " - " + WiFi.localIP().toString();
unsigned long timeoutRemainingSeconds = 0;
if (_wm->configPortalActive && _wm->_configPortalTimeout > 0) {
const unsigned long now = millis();
const unsigned long timeoutAt = _wm->_configPortalStart + _wm->_configPortalTimeout;
if (timeoutAt > now) {
timeoutRemainingSeconds = (timeoutAt - now + 999UL) / 1000UL;
}
}
jsonAppendEscaped(json, sub);
json += String(timeoutRemainingSeconds);
}
json += F("\",\"portalActive\":");
json += _wm->configPortalActive ? F("true") : F("false");
json += F(",\"features\":{");
appendPortalUiFeatureFlagsJson(json);
json += F("},\"showBack\":");
json += _wm->_showBack ? F("true") : F("false");
json += F(",\"scan\":{\"state\":\"");
json += stateStr;
json += F("\",\"count\":");
json += String((unsigned int)networks.size());
json += F("},\"initialStatus\":\"");
json += F(",\"identityText\":\"");
{
String idText;
if (_wm->_portalBrand.identityTextOverride.length() > 0) {
idText = _wm->_portalBrand.identityTextOverride;
} else if (_wm->configPortalActive) {
idText = _wm->_apName;
} else {
idText = _wm->getWiFiHostname() + " - " + WiFi.localIP().toString();
}
jsonAppendEscaped(json, idText);
}
json += F("\",\"statusSummary\":\"");
{
String st;
buildPlainStatusSummary(st);
jsonAppendEscaped(json, st);
}
json += F("\",\"scan\":{\"state\":\"");
json += stateStr;
json += F("\",\"count\":");
json += String((unsigned int)networks.size());
json += F("}}");
json += F(",\"pages\":{");
json += F("\"wifi\":{\"visible\":true}");
json += F(",\"setup\":{\"visible\":");
json += (!_wm->_portalLayout.paramsOnWifiPage && _wm->_portalPages.setupVisible) ? F("true") : F("false");
json += F("}");
json += F(",\"info\":{\"visible\":");
json += _wm->_portalPages.infoVisible ? F("true") : F("false");
json += F("}");
json += F(",\"update\":{\"visible\":");
json += _wm->_portalPages.updateVisible ? F("true") : F("false");
json += F("}}");
json += F(",\"actions\":{");
json += F("\"erase\":{\"visible\":");
json += _wm->_portalActions.eraseVisible ? F("true") : F("false");
json += F("},\"restart\":{\"visible\":");
json += (_wm->_portalActions.restartVisible && portalRunning) ? F("true") : F("false");
json += F("},\"exitPortal\":{\"visible\":");
json += (_wm->_portalActions.exitVisible && _wm->_allowExit && portalRunning) ? F("true") : F("false");
json += F("},\"closeCaptive\":{\"visible\":");
json += (_wm->_portalActions.closeCaptiveVisible && _wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false");
json += F("},\"back\":{\"visible\":");
json += _wm->_portalActions.backVisible ? F("true") : F("false");
json += F("}}");
json += F(",\"layout\":{\"paramsLocation\":\"");
json += _wm->_portalLayout.paramsOnWifiPage ? F("wifi") : F("setup");
json += F("\"}");
bool first = true;
json += F(",\"extraHomeCards\":[");
appendPortalExtraHomeCardsJson(json, first);
json += F("]}");
return json;
}
@@ -1019,10 +1177,19 @@ void WiFiManagerHandlers::handleApiWifiScan(AsyncWebServerRequest *request) {
String WiFiManagerHandlers::buildApiWifiMetaJson() {
String ssidPlaceholder = _wm->WiFi_SSID();
String passwordPlaceholder = "";
if (_wm->_showPassword) {
passwordPlaceholder = _wm->WiFi_psk();
} else if (_wm->WiFi_psk() != "") {
passwordPlaceholder = F("********");
switch (_wm->_portalPasswordPlaceholderMode) {
case PortalPasswordPlaceholderMode::Actual:
passwordPlaceholder = _wm->WiFi_psk();
break;
case PortalPasswordPlaceholderMode::Masked:
if (_wm->WiFi_psk() != "") {
passwordPlaceholder = F("********");
}
break;
case PortalPasswordPlaceholderMode::Hidden:
default:
passwordPlaceholder = "";
break;
}
String json = F("{\"wifiFields\":[");
@@ -1035,11 +1202,11 @@ String WiFiManagerHandlers::buildApiWifiMetaJson() {
appendPortalJsonStaticFields(json, first);
json += F("],\"params\":[");
first = true;
if (_wm->_paramsInWifi && _wm->getParametersCount() > 0) {
if (_wm->_portalLayout.paramsOnWifiPage && _wm->getParametersCount() > 0) {
appendPortalJsonCustomParams(json, first);
}
json += F("],\"actions\":{\"canRefreshScan\":true,\"showBack\":");
json += _wm->_showBack ? F("true") : F("false");
json += _wm->_portalActions.backVisible ? F("true") : F("false");
json += F("}}");
return json;
}
@@ -1096,7 +1263,7 @@ String WiFiManagerHandlers::buildApiParamsGetJson() {
bool first = true;
appendPortalJsonCustomParams(json, first);
json += F("],\"actions\":{\"showBack\":");
json += _wm->_showBack ? F("true") : F("false");
json += _wm->_portalActions.backVisible ? F("true") : F("false");
json += F("}}");
return json;
}
@@ -1156,6 +1323,9 @@ String WiFiManagerHandlers::buildApiInfoJson() {
first = true;
static const char *const aboutIds[] = {"aboutver", "aboutarduinover", "aboutsdkver", "aboutdate"};
appendInfoSectionFromIds(json, aboutIds, sizeof(aboutIds) / sizeof(aboutIds[0]), first);
json += F("],\"extraSections\":[");
first = true;
appendPortalExtraInfoSectionsJson(json, first);
json += F("],\"actions\":{");
appendApiInfoActionsJson(json);
json += F("}}");