소스 검색

fix: reduce repeater web panel pressure and improve mqtt diagnostics

Jared Dohrman 4 달 전
부모
커밋
de071cbb79
7개의 변경된 파일188개의 추가작업 그리고 156개의 파일을 삭제
  1. 2 0
      README.md
  2. 1 1
      docs/releases.md
  3. 30 0
      docs/web-panel.md
  4. 104 24
      src/helpers/mqtt/MQTTUplink.cpp
  5. 2 1
      src/helpers/mqtt/MQTTUplink.h
  6. 49 129
      src/helpers/web/WebPanelServer.cpp
  7. 0 1
      src/helpers/web/WebPanelServer.h

+ 2 - 0
README.md

@@ -163,6 +163,8 @@ Features include:
 - light and dark themes
 - optional disable via `set web off`
 
+Recommended use is initial setup and occasional troubleshooting. On MQTT repeater deployments, disable the panel again when finished if you want maximum MQTT heap headroom.
+
 Some constrained targets disable the panel to stay within flash limits. For example, `Xiao_C3_repeater_mqtt` builds with `WITH_WEB_PANEL=0`.
 
 ### Companion WiFi Additions

+ 1 - 1
docs/releases.md

@@ -93,7 +93,7 @@ It is the traditional way to configure a repeater after flashing, including:
 - flood max
 - some advanced repeater settings
 
-As of `v1.2.1`, the local repeater web panel also includes the same common repeater settings, so many users can continue day-to-day configuration there after initial setup.
+As of `v1.2.1`, the local repeater web panel also includes the same common repeater settings, so users can complete initial setup there and return for occasional troubleshooting or configuration changes. On MQTT repeaters that need maximum headroom, it is still best to disable the panel again when you are finished.
 
 ### Console
 

+ 30 - 0
docs/web-panel.md

@@ -17,6 +17,12 @@ It gives you:
 - editable MQTT settings
 - a stats dashboard with Wi-Fi, core, radio, memory, and packet views
 
+Operational guidance:
+
+- use it for initial setup, occasional configuration changes, and troubleshooting
+- when you are finished, prefer `set web off` on MQTT repeaters that need maximum headroom
+- this leaves more internal heap available for MQTT/WSS activity, especially on dual-broker setups
+
 ## Screenshot Overview
 
 The layout below reflects the current panel structure for the repeater web UI.
@@ -62,6 +68,18 @@ Example:
 
 This is intended for local admin use on a trusted network, not for open internet exposure.
 
+## Performance Notes
+
+The panel is designed to load more gently than earlier versions. On login it now fetches sections in sequence instead of requesting one large bootstrap payload up front.
+
+Even with that change, the panel still uses HTTPS and internal heap. On boards running one or two WSS MQTT brokers, opening the panel reduces MQTT headroom while the session is active.
+
+Recommended practice for repeater deployments:
+
+- enable the panel for initial configuration
+- use it again for occasional checks or troubleshooting
+- disable it with `set web off` when finished so MQTT has the most headroom available
+
 ## Actions
 
 The Actions panel gives you the most common operational controls:
@@ -210,6 +228,18 @@ Check:
 
 Use the repeater admin password, not the guest password.
 
+### MQTT becomes unstable when I log in
+
+The web panel now loads settings section-by-section to reduce startup pressure, but HTTPS still consumes internal heap.
+
+Check:
+
+- whether one or two MQTT brokers are enabled
+- `memory` before and after login
+- whether stability improves after `set web off`
+
+For fixed installations where MQTT uptime matters more than browser access, use the panel briefly and then disable it again.
+
 ### A command says it is not allowlisted
 
 The panel intentionally limits what can be run from the browser. Use the serial CLI for commands outside the web allowlist. `clock` is included, but most maintenance and debug commands are still serial-only.

+ 104 - 24
src/helpers/mqtt/MQTTUplink.cpp

@@ -120,6 +120,25 @@ const char* getWifiStateLabel(const MQTTPrefs& prefs, bool wifi_started) {
   return "down";
 }
 
+#if MQTT_DEBUG
+void logMqttMemorySnapshot(const char* phase, const char* broker_label = nullptr) {
+  MQTT_LOG("mem phase=%s broker=%s uptime_ms=%lu heap_free=%u heap_min=%u heap_max=%u psram_free=%u psram_min=%u "
+           "psram_max=%u",
+           phase != nullptr ? phase : "-",
+           broker_label != nullptr ? broker_label : "-",
+           millis(),
+           ESP.getFreeHeap(),
+           ESP.getMinFreeHeap(),
+           ESP.getMaxAllocHeap(),
+           ESP.getFreePsram(),
+           ESP.getMinFreePsram(),
+           ESP.getMaxAllocPsram());
+}
+#else
+void logMqttMemorySnapshot(const char*, const char* = nullptr) {
+}
+#endif
+
 }
 
 const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[3] = {
@@ -398,6 +417,14 @@ bool MQTTUplink::refreshToken(BrokerState& broker) {
 }
 
 void MQTTUplink::destroyBroker(BrokerState& broker, bool reset_retry_state) {
+  bool had_runtime_state = broker.client != nullptr || broker.token != nullptr || broker.connected ||
+                           broker.connect_announced || broker.reconnect_pending || broker.next_connect_attempt != 0 ||
+                           broker.last_connect_attempt != 0 || broker.reconnect_failures != 0 ||
+                           broker.token_expires_at != 0;
+  if (!had_runtime_state) {
+    return;
+  }
+  logMqttMemorySnapshot("destroy-pre", broker.spec != nullptr ? broker.spec->label : nullptr);
   if (broker.client != nullptr) {
     MQTT_LOG("%s destroy broker client", broker.spec->label);
     esp_mqtt_client_stop(broker.client);
@@ -408,6 +435,7 @@ void MQTTUplink::destroyBroker(BrokerState& broker, bool reset_retry_state) {
   broker.token = nullptr;
   broker.connected = false;
   broker.connect_announced = false;
+  broker.connected_since_ms = 0;
   broker.token_expires_at = 0;
   if (reset_retry_state) {
     broker.reconnect_pending = false;
@@ -415,6 +443,7 @@ void MQTTUplink::destroyBroker(BrokerState& broker, bool reset_retry_state) {
     broker.reconnect_failures = 0;
     broker.last_connect_attempt = 0;
   }
+  logMqttMemorySnapshot("destroy-post", broker.spec != nullptr ? broker.spec->label : nullptr);
 }
 
 void MQTTUplink::queuePublish(BrokerState& broker, const char* topic, const char* payload, bool retain) {
@@ -423,7 +452,8 @@ void MQTTUplink::queuePublish(BrokerState& broker, const char* topic, const char
   }
   MQTT_LOG("%s publish topic=%s retain=%d bytes=%u", broker.spec->label, topic, retain ? 1 : 0,
            static_cast<unsigned>(strlen(payload)));
-  esp_mqtt_client_enqueue(broker.client, topic, payload, 0, 1, retain, true);
+  int enqueue_rc = esp_mqtt_client_enqueue(broker.client, topic, payload, 0, 1, retain, true);
+  MQTT_LOG("%s enqueue topic=%s rc=%d connected=%d", broker.spec->label, topic, enqueue_rc, broker.connected ? 1 : 0);
 }
 
 int MQTTUplink::buildStatusJson(char* buffer, size_t buffer_size, bool online) const {
@@ -565,6 +595,7 @@ void MQTTUplink::publishOnlineStatus(BrokerState& broker) {
 }
 
 void MQTTUplink::publishStatus(bool online) {
+  logMqttMemorySnapshot(online ? "status-pre" : "status-offline-pre");
   char* payload = allocScratchBuffer(768);
   if (payload == nullptr) {
     return;
@@ -580,6 +611,7 @@ void MQTTUplink::publishStatus(bool online) {
     }
   }
   freeScratchBuffer(payload);
+  logMqttMemorySnapshot(online ? "status-post" : "status-offline-post");
 }
 
 void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t event_id, void* event_data) {
@@ -588,6 +620,8 @@ void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t e
     return;
   }
   auto* event = static_cast<esp_mqtt_event_handle_t>(event_data);
+  unsigned long now_ms = millis();
+  unsigned long connected_for_ms = broker->connected_since_ms != 0 ? (now_ms - broker->connected_since_ms) : 0;
 
   switch (event_id) {
     case MQTT_EVENT_CONNECTED:
@@ -595,33 +629,56 @@ void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t e
       broker->reconnect_pending = false;
       broker->next_connect_attempt = 0;
       broker->reconnect_failures = 0;
+      broker->connected_since_ms = now_ms;
       MQTT_LOG("%s connected", broker->spec->label);
+      logMqttMemorySnapshot("connected", broker->spec->label);
       break;
     case MQTT_EVENT_DISCONNECTED:
-      MQTT_LOG("%s disconnected", broker->spec->label);
+      broker->connected = false;
+      broker->connected_since_ms = 0;
+      MQTT_LOG("%s disconnected wifi_status=%d rssi=%d connected_for_ms=%lu", broker->spec->label,
+               static_cast<int>(WiFi.status()), WiFi.RSSI(), connected_for_ms);
+      logMqttMemorySnapshot("disconnected", broker->spec->label);
+      if (!broker->reconnect_pending) {
+        if (broker->reconnect_failures < 10) {
+          broker->reconnect_failures++;
+        }
+        broker->reconnect_pending = true;
+        broker->next_connect_attempt = now_ms + getBrokerRetryDelayMillis(broker->reconnect_failures);
+        MQTT_LOG("%s reconnect in %lu ms (failures=%u)", broker->spec->label,
+                 getBrokerRetryDelayMillis(broker->reconnect_failures),
+                 static_cast<unsigned>(broker->reconnect_failures));
+      }
+      break;
     case MQTT_EVENT_ERROR:
       broker->connected = false;
-      if (broker->reconnect_failures < 10) {
-        broker->reconnect_failures++;
+      broker->connected_since_ms = 0;
+      if (event != nullptr && event->error_handle != nullptr) {
+        MQTT_LOG("%s error type=%d tls_esp=0x%x tls_stack=0x%x cert_flags=0x%x sock_errno=%d conn_refused=%d "
+                 "connected_for_ms=%lu",
+                 broker->spec->label, event->error_handle->error_type, event->error_handle->esp_tls_last_esp_err,
+                 event->error_handle->esp_tls_stack_err, event->error_handle->esp_tls_cert_verify_flags,
+                 event->error_handle->esp_transport_sock_errno, event->error_handle->connect_return_code,
+                 connected_for_ms);
+      } else {
+        MQTT_LOG("%s error event connected_for_ms=%lu", broker->spec->label, connected_for_ms);
       }
-      broker->reconnect_pending = true;
-      broker->next_connect_attempt = millis() + getBrokerRetryDelayMillis(broker->reconnect_failures);
-      if (event_id == MQTT_EVENT_ERROR) {
-        if (event != nullptr && event->error_handle != nullptr) {
-          MQTT_LOG("%s error type=%d tls_esp=0x%x tls_stack=0x%x cert_flags=0x%x sock_errno=%d conn_refused=%d",
-                   broker->spec->label, event->error_handle->error_type, event->error_handle->esp_tls_last_esp_err,
-                   event->error_handle->esp_tls_stack_err, event->error_handle->esp_tls_cert_verify_flags,
-                   event->error_handle->esp_transport_sock_errno, event->error_handle->connect_return_code);
-        } else {
-          MQTT_LOG("%s error event", broker->spec->label);
+      MQTT_LOG("%s wifi_status=%d rssi=%d", broker->spec->label, static_cast<int>(WiFi.status()), WiFi.RSSI());
+      logMqttMemorySnapshot("error", broker->spec->label);
+      if (!broker->reconnect_pending) {
+        if (broker->reconnect_failures < 10) {
+          broker->reconnect_failures++;
         }
+        broker->reconnect_pending = true;
+        broker->next_connect_attempt = now_ms + getBrokerRetryDelayMillis(broker->reconnect_failures);
+        MQTT_LOG("%s reconnect in %lu ms (failures=%u)", broker->spec->label,
+                 getBrokerRetryDelayMillis(broker->reconnect_failures),
+                 static_cast<unsigned>(broker->reconnect_failures));
       }
-      MQTT_LOG("%s reconnect in %lu ms (failures=%u)", broker->spec->label,
-               getBrokerRetryDelayMillis(broker->reconnect_failures),
-               static_cast<unsigned>(broker->reconnect_failures));
       break;
     case MQTT_EVENT_BEFORE_CONNECT:
       MQTT_LOG("%s before connect", broker->spec->label);
+      logMqttMemorySnapshot("before-connect", broker->spec->label);
       break;
     default:
       break;
@@ -716,13 +773,17 @@ void MQTTUplink::updateTimeSync() {
   }
 }
 
-void MQTTUplink::ensureBroker(BrokerState& broker) {
+void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
   if (broker.spec == nullptr) {
     return;
   }
   bool enabled = (_prefs.enabled_mask & broker.spec->bit) != 0;
   if (!enabled) {
-    destroyBroker(broker);
+    if (broker.client != nullptr || broker.token != nullptr || broker.connected || broker.connect_announced ||
+        broker.reconnect_pending || broker.next_connect_attempt != 0 || broker.last_connect_attempt != 0 ||
+        broker.reconnect_failures != 0 || broker.token_expires_at != 0) {
+      destroyBroker(broker);
+    }
     return;
   }
 
@@ -749,6 +810,9 @@ void MQTTUplink::ensureBroker(BrokerState& broker) {
   if (broker.next_connect_attempt != 0 && now_ms < broker.next_connect_attempt) {
     return;
   }
+  if (!allow_new_connect) {
+    return;
+  }
   broker.last_connect_attempt = now_ms;
   broker.reconnect_pending = false;
 
@@ -759,8 +823,9 @@ void MQTTUplink::ensureBroker(BrokerState& broker) {
   }
 
   refreshBrokerState(broker);
-  MQTT_LOG("%s mqtt init host=%s port=%d path=%s client_id=%s heap_free=%u heap_max=%u", broker.spec->label,
-           broker.spec->host, 443, "/mqtt", broker.client_id, ESP.getFreeHeap(), ESP.getMaxAllocHeap());
+  MQTT_LOG("%s mqtt init host=%s port=%d path=%s client_id=%s", broker.spec->label, broker.spec->host, 443, "/mqtt",
+           broker.client_id);
+  logMqttMemorySnapshot("init-pre", broker.spec->label);
   esp_mqtt_client_config_t cfg = {};
 #if ESP_IDF_VERSION_MAJOR >= 5
   cfg.broker.address.hostname = broker.spec->host;
@@ -805,17 +870,21 @@ void MQTTUplink::ensureBroker(BrokerState& broker) {
   broker.client = esp_mqtt_client_init(&cfg);
   if (broker.client == nullptr) {
     MQTT_LOG("%s mqtt init failed", broker.spec->label);
+    logMqttMemorySnapshot("init-failed", broker.spec->label);
     return;
   }
+  logMqttMemorySnapshot("init-post", broker.spec->label);
 
   esp_mqtt_client_register_event(broker.client, MQTT_EVENT_ANY, &MQTTUplink::handleMqttEvent, &broker);
   if (esp_mqtt_client_start(broker.client) != ESP_OK) {
     MQTT_LOG("%s mqtt start failed", broker.spec->label);
+    logMqttMemorySnapshot("start-failed", broker.spec->label);
     broker.reconnect_pending = true;
     broker.next_connect_attempt = now_ms + kBrokerRetryBaseMillis;
     destroyBroker(broker, false);
   } else {
     MQTT_LOG("%s mqtt start requested", broker.spec->label);
+    logMqttMemorySnapshot("start-requested", broker.spec->label);
   }
 }
 
@@ -865,8 +934,21 @@ void MQTTUplink::loop(const MQTTStatusSnapshot& snapshot) {
     _web_panel.lockSession();
   }
 
+  BrokerState* active_connecting_broker = nullptr;
+  for (BrokerState& broker : _brokers) {
+    if (broker.client != nullptr && !broker.connected && !broker.reconnect_pending) {
+      active_connecting_broker = &broker;
+      break;
+    }
+  }
+
+  bool connect_started = false;
   for (BrokerState& broker : _brokers) {
-    ensureBroker(broker);
+    bool allow_new_connect = active_connecting_broker == nullptr && !connect_started;
+    ensureBroker(broker, allow_new_connect);
+    if (active_connecting_broker == nullptr && broker.client != nullptr && !broker.connected && !broker.reconnect_pending) {
+      connect_started = true;
+    }
     if (broker.connected && !broker.connect_announced) {
       publishOnlineStatus(broker);
       broker.connect_announced = true;
@@ -1041,8 +1123,6 @@ bool MQTTUplink::setWebEnabled(bool enabled) {
   bool ok = savePrefs();
   if (_prefs.web_enabled != 0) {
     ensureWebServer();
-  } else {
-    stopWebServer();
   }
   return ok;
 #else

+ 2 - 1
src/helpers/mqtt/MQTTUplink.h

@@ -98,6 +98,7 @@ private:
     bool reconnect_pending;
     unsigned long last_connect_attempt;
     unsigned long next_connect_attempt;
+    unsigned long connected_since_ms;
     time_t token_expires_at;
     uint8_t reconnect_failures;
     char username[70];
@@ -144,7 +145,7 @@ private:
   void refreshIdentityStrings();
   void refreshBrokerIdentity(BrokerState& broker);
   void refreshBrokerState(BrokerState& broker);
-  void ensureBroker(BrokerState& broker);
+  void ensureBroker(BrokerState& broker, bool allow_new_connect);
   void destroyBroker(BrokerState& broker, bool reset_retry_state = true);
   bool refreshToken(BrokerState& broker);
   void publishStatus(bool online);

+ 49 - 129
src/helpers/web/WebPanelServer.cpp

@@ -1219,8 +1219,8 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
       const value = document.getElementById(inputId).value;
       runCommand(prefix + value);
     }
-    async function loadField(cmd, inputId, format) {
-      const result = await runCommand(cmd);
+    async function loadField(cmd, inputId, format, options = {}) {
+      const result = await runCommand(cmd, options);
       if (!result.ok) return;
       let value = parseReplyValue(result.text);
       if (format === "multiline") {
@@ -1285,27 +1285,6 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
         }
         return JSON.parse(text);
       });
-    }
-    function applyBootstrapData(data) {
-      if (!data) return;
-	      if (typeof data.name === "string") document.getElementById("nodeName").value = data.name;
-	      if (typeof data.mqtt_iata === "string" && data.mqtt_iata.length) document.getElementById("mqttIata").value = data.mqtt_iata;
-	      if (typeof data.mqtt_owner === "string") document.getElementById("mqttOwner").value = data.mqtt_owner;
-	      if (typeof data.mqtt_email === "string") document.getElementById("mqttEmail").value = data.mqtt_email;
-	      if (typeof data.advert_interval === "string") document.getElementById("advertInterval").value = data.advert_interval;
-	      if (typeof data.flood_interval === "string") document.getElementById("floodInterval").value = data.flood_interval;
-	      if (typeof data.flood_max === "string") document.getElementById("floodMax").value = data.flood_max;
-	      if (typeof data.path_hash_mode === "string") document.getElementById("pathHashMode").value = data.path_hash_mode;
-	      if (typeof data.private_key === "string") document.getElementById("privateKey").value = data.private_key.toUpperCase();
-	      if (typeof data.radio === "string") currentRadioConfig = parseRadioValue(data.radio);
-	      if (typeof data.role === "string") document.getElementById("roleValue").value = data.role;
-	      if (typeof data.clock === "string") document.getElementById("clockUtc").value = data.clock;
-	      if (typeof data.public_key === "string") document.getElementById("publicKey").value = data.public_key.toUpperCase();
-      if (typeof data.mqtt_eastmesh_au === "string") setBrokerToggle("mqttEastmeshAu", data.mqtt_eastmesh_au);
-      if (typeof data.mqtt_letsmesh_eu === "string") setBrokerToggle("mqttLetsmeshEu", data.mqtt_letsmesh_eu);
-      if (typeof data.mqtt_letsmesh_us === "string") setBrokerToggle("mqttLetsmeshUs", data.mqtt_letsmesh_us);
-      syncRadioPresetUi();
-      updateBrokerWarning();
     }
 	    function getLetsmeshMode() {
 	      const eu = document.getElementById("mqttLetsmeshEu");
@@ -1376,17 +1355,13 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
 	        refreshLetsmeshModeUi();
 	      }
 	    }
-	    function updateBrokerWarning() {
-	      document.getElementById("mqttBrokerWarning").textContent = "";
-	    }
-    async function loadBrokerState(cmd, inputId) {
-      const result = await runCommand(cmd);
+    async function loadBrokerState(cmd, inputId, options = {}) {
+      const result = await runCommand(cmd, options);
       if (!result.ok) return;
       setBrokerToggle(inputId, parseReplyValue(result.text));
-      updateBrokerWarning();
     }
-    async function loadRadioConfig() {
-      const result = await runCommand("get radio");
+    async function loadRadioConfig(options = {}) {
+      const result = await runCommand("get radio", options);
       if (!result.ok) {
         setRadioPresetStatus("Unable to load current radio config.", true);
         return;
@@ -1400,6 +1375,16 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
       setRadioPresetStatus("");
       syncRadioPresetUi();
     }
+    function pause(ms) {
+      return new Promise((resolve) => setTimeout(resolve, ms));
+    }
+    async function loadSection(title, tasks) {
+      statusEl.textContent = title;
+      for (const task of tasks) {
+        await task();
+      }
+      await pause(40);
+    }
     async function loadRadioPresets() {
       const selectEl = document.getElementById("radioPreset");
       const applyBtn = document.getElementById("applyRadioPresetBtn");
@@ -1493,13 +1478,11 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
 	      if (!result.ok) {
 	        refreshEastmeshModeUi();
 	        refreshLetsmeshModeUi();
-	        updateBrokerWarning();
 	        return;
 	      }
 	      setBrokerToggle("mqttEastmeshAu", enabled ? "on" : "off");
 	      refreshEastmeshModeUi();
 	      refreshLetsmeshModeUi();
-	      updateBrokerWarning();
 	    }
 	    const eastmeshModeSlider = document.getElementById("mqttEastmeshMode");
 	    if (eastmeshModeSlider) {
@@ -1513,7 +1496,6 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
 	    async function setLetsmeshMode(mode) {
 	      const eastmesh = document.getElementById("mqttEastmeshAu");
 	      if (mode === "both" && eastmesh && eastmesh.checked) {
-	        updateBrokerWarning();
 	        refreshLetsmeshModeUi();
 	        return;
 	      }
@@ -1532,13 +1514,11 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
 	        const result = await runCommand(command);
 	        if (!result.ok) {
 	          refreshLetsmeshModeUi();
-	          updateBrokerWarning();
 	          return;
 	        }
 	        setBrokerToggle(inputId, nextState);
 	      }
 	      refreshLetsmeshModeUi();
-	      updateBrokerWarning();
 	    }
 	    const letsmeshModeSlider = document.getElementById("mqttLetsmeshMode");
 	    if (letsmeshModeSlider) {
@@ -1615,30 +1595,43 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
         return;
       }
       showAuthedUi(true);
+      const quiet = { recordHistory:false, updateInput:false };
       try {
-        applyBootstrapData(await fetchJson("/api/bootstrap"));
-      } catch (_) {
+        await loadSection("Loading info...", [
+          () => loadField("get role", "roleValue", null, quiet),
+          () => loadField("clock", "clockUtc", null, quiet),
+          () => loadField("get public.key", "publicKey", "uppercase", quiet)
+        ]);
+        await loadSection("Loading repeater settings...", [
+          () => loadField("get name", "nodeName", null, quiet),
+          () => loadField("get lat", "nodeLat", null, quiet),
+          () => loadField("get lon", "nodeLon", null, quiet),
+          () => loadField("get prv.key", "privateKey", "uppercase", quiet),
+          () => loadField("get owner.info", "ownerInfo", "multiline", quiet)
+        ]);
+        await loadSection("Loading radio settings...", [
+          () => loadRadioConfig(quiet),
+          () => loadField("get path.hash.mode", "pathHashMode", null, quiet)
+        ]);
+        await loadSection("Loading advertising...", [
+          () => loadField("get advert.interval", "advertInterval", null, quiet),
+          () => loadField("get flood.advert.interval", "floodInterval", null, quiet),
+          () => loadField("get flood.max", "floodMax", null, quiet)
+        ]);
+        await loadSection("Loading MQTT settings...", [
+          () => loadField("get mqtt.iata", "mqttIata", null, quiet),
+          () => loadField("get mqtt.owner", "mqttOwner", null, quiet),
+          () => loadField("get mqtt.email", "mqttEmail", null, quiet),
+          () => loadBrokerState("get mqtt.eastmesh-au", "mqttEastmeshAu", quiet),
+          () => loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu", quiet),
+          () => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet)
+        ]);
+        statusEl.textContent = "Ready";
+      } catch (error) {
         if (!token) {
           return;
         }
-        await Promise.all([
-          loadField("get name", "nodeName"),
-          loadField("get mqtt.iata", "mqttIata"),
-          loadField("get mqtt.owner", "mqttOwner"),
-          loadField("get mqtt.email", "mqttEmail"),
-          loadBrokerState("get mqtt.eastmesh-au", "mqttEastmeshAu"),
-          loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu"),
-          loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs"),
-          loadField("get advert.interval", "advertInterval"),
-          loadField("get flood.advert.interval", "floodInterval"),
-          loadField("get flood.max", "floodMax"),
-          loadField("get path.hash.mode", "pathHashMode"),
-          loadField("get prv.key", "privateKey", "uppercase"),
-          loadField("get role", "roleValue"),
-          loadField("clock", "clockUtc"),
-          loadField("get public.key", "publicKey", "uppercase"),
-          loadRadioConfig()
-        ]);
+        statusEl.textContent = error && error.message ? error.message : "Unable to load repeater settings.";
       }
       loadRadioPresets();
 	    }
@@ -1697,13 +1690,11 @@ bool WebPanelServer::start() {
   httpd_uri_t app_uri = {.uri = "/app", .method = HTTP_GET, .handler = &WebPanelServer::handleApp, .user_ctx = &_route_context};
   httpd_uri_t login_uri = {.uri = "/login", .method = HTTP_POST, .handler = &WebPanelServer::handleLogin, .user_ctx = &_route_context};
   httpd_uri_t command_uri = {.uri = "/api/command", .method = HTTP_POST, .handler = &WebPanelServer::handleCommand, .user_ctx = &_route_context};
-  httpd_uri_t bootstrap_uri = {.uri = "/api/bootstrap", .method = HTTP_GET, .handler = &WebPanelServer::handleBootstrap, .user_ctx = &_route_context};
   httpd_uri_t stats_uri = {.uri = "/api/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStats, .user_ctx = &_route_context};
   httpd_register_uri_handler(_server, &index_uri);
   httpd_register_uri_handler(_server, &app_uri);
   httpd_register_uri_handler(_server, &login_uri);
   httpd_register_uri_handler(_server, &command_uri);
-  httpd_register_uri_handler(_server, &bootstrap_uri);
   httpd_register_uri_handler(_server, &stats_uri);
   WEB_PANEL_LOG("server started on https://%s/", WiFi.localIP().toString().c_str());
   return true;
@@ -1826,77 +1817,6 @@ esp_err_t WebPanelServer::handleCommand(httpd_req_t* req) {
   return rc;
 }
 
-esp_err_t WebPanelServer::handleBootstrap(httpd_req_t* req) {
-  auto* ctx = static_cast<RouteContext*>(req->user_ctx);
-  if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {
-    return httpd_resp_send_500(req);
-  }
-  if (!ctx->self->isAuthorized(req)) {
-    return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
-  }
-
-  ctx->self->noteActivity();
-  char* reply = allocScratchBuffer(kWebReplyBufferSize);
-  if (reply == nullptr) {
-    freeScratchBuffer(reply);
-    return httpd_resp_send_500(req);
-  }
-
-  const struct {
-    const char* key;
-    const char* command;
-	  } fields[] = {
-	      {"name", "get name"},
-	      {"mqtt_iata", "get mqtt.iata"},
-	      {"mqtt_owner", "get mqtt.owner"},
-	      {"mqtt_email", "get mqtt.email"},
-	      {"mqtt_eastmesh_au", "get mqtt.eastmesh-au"},
-	      {"mqtt_letsmesh_eu", "get mqtt.letsmesh-eu"},
-	      {"mqtt_letsmesh_us", "get mqtt.letsmesh-us"},
-	      {"advert_interval", "get advert.interval"},
-	      {"flood_interval", "get flood.advert.interval"},
-	      {"flood_max", "get flood.max"},
-        {"path_hash_mode", "get path.hash.mode"},
-        {"private_key", "get prv.key"},
-        {"role", "get role"},
-        {"clock", "clock"},
-        {"public_key", "get public.key"},
-        {"radio", "get radio"},
-	  };
-
-  httpd_resp_set_type(req, "application/json; charset=utf-8");
-  httpd_resp_set_hdr(req, "Cache-Control", "no-store");
-  if (sendChunk(req, "{") != ESP_OK) {
-    freeScratchBuffer(reply);
-    httpd_resp_sendstr_chunk(req, nullptr);
-    return ESP_FAIL;
-  }
-  for (size_t i = 0; i < (sizeof(fields) / sizeof(fields[0])); ++i) {
-    memset(reply, 0, kWebReplyBufferSize);
-    ctx->self->_runner->runWebCommand(fields[i].command, reply, kWebReplyBufferSize);
-    const char* value = reply;
-    if (value[0] == '>' && value[1] == ' ') {
-      value += 2;
-    }
-    if (strcmp(value, "-") == 0) {
-      value = "";
-    }
-    if (sendJsonFieldChunk(req, fields[i].key, value, i != 0) != ESP_OK) {
-      freeScratchBuffer(reply);
-      httpd_resp_sendstr_chunk(req, nullptr);
-      return ESP_FAIL;
-    }
-  }
-  esp_err_t rc = sendChunk(req, "}");
-  if (rc == ESP_OK) {
-    rc = httpd_resp_sendstr_chunk(req, nullptr);
-  } else {
-    httpd_resp_sendstr_chunk(req, nullptr);
-  }
-  freeScratchBuffer(reply);
-  return rc;
-}
-
 esp_err_t WebPanelServer::handleStats(httpd_req_t* req) {
   auto* ctx = static_cast<RouteContext*>(req->user_ctx);
   if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {

+ 0 - 1
src/helpers/web/WebPanelServer.h

@@ -47,7 +47,6 @@ private:
   static esp_err_t handleApp(httpd_req_t* req);
   static esp_err_t handleLogin(httpd_req_t* req);
   static esp_err_t handleCommand(httpd_req_t* req);
-  static esp_err_t handleBootstrap(httpd_req_t* req);
   static esp_err_t handleStats(httpd_req_t* req);
 
   bool readRequestBody(httpd_req_t* req, char* buffer, size_t buffer_size) const;