Merge pull request #55 from xJARiD/develop

fix: gps, truncated LWT, improve no-psram stats, web stability, and absent sensor probes
This commit is contained in:
xJARiD authored and GitHub committed 2026-04-30 15:21:53 +10:00
commit 37a8dd0633
10 files changed
+212 -47

No files matched your search

+15
View File
@@ -102,6 +102,21 @@ Example:
curl -sk -X POST https://<repeater-ip>/login --data '<admin-password>'
```
### `GET /api/session`
Check whether a stored session token is still valid.
Headers:
```text
X-Auth-Token: <token>
```
Response:
- JSON `{"authenticated":true}` on success
- `401 Unauthorized` if the token is missing or stale after lock, restart, or reflash
### `POST /api/command`
Run a repeater CLI command remotely.
+1 -1
View File
@@ -70,7 +70,7 @@ Legacy dotted aliases are also accepted:
- `get web`
- `get web.status`: shows whether the local HTTPS panel is available.
- `get web.stats.status`: shows whether the dedicated stats page and history subsystem are enabled, whether recent history is active, whether PSRAM-backed history is available, and whether the SD-backed archive is mounted. When enabled, the history capture now covers supported environment telemetry too, not just the original battery/radio series. GPS-enabled boards also record per-minute satellites samples for the `/stats` history view.
- `get web.stats.status`: shows whether the dedicated stats page and history subsystem are enabled, whether recent history is active, whether PSRAM-backed history is available, and whether the SD-backed archive is mounted. When enabled, the history capture now covers supported environment telemetry too, not just the original battery/radio series. GPS-active boards also record per-minute satellites samples for the `/stats` history view.
- `set web on|off`
- `set.web on|off`: enables or disables the local HTTPS panel.
- `set web.stats on|off`
+7 -5
View File
@@ -72,6 +72,8 @@ Example:
- the connection is HTTPS, but the certificate is self-signed
- browsers will warn the first time you connect
- the panel exposes the repeater CLI after login
- the browser keeps the session token while changing between `/app` and `/stats`; the page switches views in place until logout, idle lock, or server restart
- after a device restart or reflash, the app checks the stored token before loading settings and returns to login if the session is stale
This is intended for local admin use on a trusted network, not for open internet exposure.
@@ -229,13 +231,13 @@ The `/stats` page currently shows:
- `Services`: MQTT, web, archive, neighbour count, and, when mounted, card and archive capacity
- optional full-width `Environment` summary card on boards that report GPS or environmental telemetry
- `Trends`: battery, heap free, packet activity, signal, noise floor, and, when GPS is enabled, satellites
- `Trends`: battery, heap free, packet activity, signal, noise floor, and, when GPS is active, satellites
- `Neighbours`: current neighbour table with ID, SNR, heard age, and advert age
- `Events`: current boot/session events
Planned MQTT JWT rotations are shown as `mqtt_token_refreshed` events instead of a short `mqtt_disconnected` / `mqtt_connected` pair.
For boards that expose extra telemetry, the optional `Environment` summary card can show current values such as GPS fix state, latitude, longitude, GPS altitude, voltage, sensor temperature, humidity, barometer, pressure-derived altitude, and MCU temperature.
For boards that expose extra telemetry, the optional `Environment` summary card can show current values such as GPS active/fix state, latitude, longitude, GPS altitude, voltage, sensor temperature, humidity, barometer, pressure-derived altitude, and MCU temperature.
Metrics with no current value are hidden rather than showing placeholder rows, so the cards vary by board and by current sensor state.
@@ -248,7 +250,7 @@ The trend graphs load sequentially rather than as one large payload:
3. memory
4. packet activity
5. signal
6. satellites when GPS is enabled
6. satellites when GPS is active
This keeps browser-side and device-side memory use lower than the previous in-page stats view.
@@ -267,13 +269,13 @@ Current in-memory history caps are:
| `4 MB` to less than `8 MB` PSRAM | `480` | `192` | About `8` hours |
| `8 MB` PSRAM or more | `720` | `288` | About `12` hours |
On boards with roughly `2 MB` PSRAM or more, stats history starts capturing from boot when `web.stats` is enabled, even if `/stats` has not been opened yet.
When `web.stats` is enabled, stats history starts capturing from boot or from the time it is enabled, even if `/stats` has not been opened yet. No-PSRAM boards use the smaller live-only buffer shown above.
Archive-backed restore requires `web.stats` enabled plus a mounted SD card on boards that support the EastMesh archive path.
The main purpose of the SD card is to let the repeater retain and restore stats history for `/stats`. The archive keeps fast `.latest` snapshot files for quick restore and UTC-dated daily `.log` files for longer-term history. As a secondary option, those files can also be removed and inspected on a computer for deeper manual review.
On no-PSRAM boards, `/stats` can still show recent graphs while the stats view is active, but the history is smaller and does not provide the same archive-backed behaviour as PSRAM-capable boards.
On no-PSRAM boards, `/stats` can still show recent graphs, but the history is smaller and does not provide the same archive-backed behaviour as PSRAM-capable boards.
Useful CLI commands:
+6 -3
View File
@@ -55,10 +55,13 @@ WebSensorSnapshot collectWebSensorSnapshot(mesh::MainBoard& board, SensorManager
LocationProvider* location = sensors.getLocationProvider();
if (location != nullptr) {
snapshot.has_gps = true;
snapshot.gps_enabled = location->isEnabled();
snapshot.gps_fix = location->isValid();
const bool provider_enabled = location->isEnabled();
const char* gps_setting = sensors.getSettingByKey("gps");
const bool setting_active = (gps_setting == nullptr) || (strcmp(gps_setting, "1") == 0);
snapshot.gps_enabled = provider_enabled && setting_active;
snapshot.gps_fix = snapshot.gps_enabled && location->isValid();
const long satellites = location->satellitesCount();
if (snapshot.gps_enabled || satellites > 0) {
if (snapshot.gps_enabled) {
snapshot.has_satellites = true;
snapshot.satellites = max<long>(satellites, 0);
}
+30
View File
@@ -527,3 +527,33 @@ releases:
area: readme
text: "Updated the README to describe EastMesh as an Eastern Australia community mesh network, introduce CoreScope telemetry, and warn that MQTT repeater builds are intended for Eastern Australia repeaters only."
breaking_changes: []
- track: repeater-mqtt
version: "1.3.14"
tag: "repeater-mqtt-eastmesh-v1.3.14"
date: "2026-04-30"
previous_version: "1.3.13"
summary: "Fixes `/stats` GPS visibility and no-PSRAM trend capture, improves web-panel session recovery and App/Stats navigation on constrained boards, quiets absent I2C sensor probes, and prevents truncated MQTT offline status payloads."
changes:
- type: fixed
area: stats
text: "Updated `/stats` GPS gauges and satellite trends to respect the active GPS setting so boards that report GPS hardware as on but deactivated after `gps off` no longer keep satellite metrics visible."
- type: fixed
area: stats
text: "Allowed no-PSRAM boards to collect the small live `/stats` history buffer whenever `web.stats` is enabled, so Heltec v3 targets can show recent trend samples without archive storage."
- type: fixed
area: web-panel
text: "Improved App/Stats navigation and session recovery by preserving the login token across panel views, validating stale browser tokens before loading settings, and clearing expired sessions quickly after a reflash."
- type: fixed
area: web-panel
text: "Delayed the red MQTT IATA warning until the panel has loaded the configured value, avoiding a misleading unset banner while `/app` is still fetching settings."
- type: fixed
area: sensors
text: "Probed optional I2C environmental sensor addresses before driver initialization so boards without those sensors no longer emit repeated Wire read errors during boot."
- type: fixed
area: mqtt
text: "Increased the MQTT offline last-will payload buffer so the retained offline status JSON is not truncated when release metadata and identity fields are near their configured limits."
- type: docs
area: web-panel
text: "Documented no-PSRAM `/stats` history behavior, stale browser-token recovery, and the lightweight `/api/session` endpoint used by the web panel."
breaking_changes: []
+2 -2
View File
@@ -537,7 +537,7 @@ bool StatsHistory::supportsPersistence() const {
}
bool StatsHistory::shouldAutoActivateFromBoot() const {
return _enabled && !_live_only && hasBootAutoCapturePsram();
return _enabled && (_live_only || hasBootAutoCapturePsram());
}
bool StatsHistory::isBootAutoCaptureExpected() const {
@@ -556,7 +556,7 @@ bool StatsHistory::isAccessActive(uint32_t now_ms) const {
if (!_live_only) {
return true;
}
return _last_access_ms != 0 && (now_ms - _last_access_ms) < kLiveOnlyIdleTimeoutMs;
return _activated || (_last_access_ms != 0 && (now_ms - _last_access_ms) < kLiveOnlyIdleTimeoutMs);
}
void StatsHistory::noteAccess(uint32_t now_ms) {
+1 -1
View File
@@ -94,7 +94,7 @@ private:
char* token;
char client_id[48];
char status_topic[128];
char offline_payload[256];
char offline_payload[512];
};
#endif
@@ -1,5 +1,7 @@
#include "EnvironmentSensorManager.h"
#include <Wire.h>
#if ENV_PIN_SDA && ENV_PIN_SCL
#define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors
#else
@@ -161,6 +163,18 @@ public:
static RAK12500LocationProvider RAK12500_provider;
#endif
namespace {
bool i2cDevicePresent(TwoWire* wire, uint8_t address) {
if (wire == nullptr) {
return false;
}
wire->beginTransmission(address);
return wire->endTransmission() == 0;
}
} // namespace
bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_GPS
#ifdef RAK_WISBLOCK_GPS
@@ -182,7 +196,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_AHTX0
if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_AHTX_ADDRESS) && AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
AHTX0_initialized = true;
} else {
@@ -192,7 +206,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_BME680
if (BME680.begin(TELEM_BME680_ADDRESS)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_BME680_ADDRESS) && BME680.begin(TELEM_BME680_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
BME680_initialized = true;
} else {
@@ -202,7 +216,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_BME280
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_BME280_ADDRESS) && BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
// Reduce self-heating: single-shot conversions, light oversampling, long standby.
@@ -220,7 +234,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_BMP280
if (BMP280.begin(TELEM_BMP280_ADDRESS)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_BMP280_ADDRESS) && BMP280.begin(TELEM_BMP280_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESS);
MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID());
BMP280_initialized = true;
@@ -231,7 +245,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_SHTC3
if (SHTC3.begin(TELEM_WIRE)) {
if (i2cDevicePresent(TELEM_WIRE, 0x70) && SHTC3.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
SHTC3_initialized = true;
} else {
@@ -242,10 +256,14 @@ bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_SHT4X
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
uint32_t serialNumber = 0;
int16_t sht4x_error;
sht4x_error = SHT4X.serialNumber(serialNumber);
if (i2cDevicePresent(TELEM_WIRE, TELEM_SHT4X_ADDRESS)) {
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
sht4x_error = SHT4X.serialNumber(serialNumber);
} else {
sht4x_error = -1;
}
if (sht4x_error == 0) {
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
SHT4X_initialized = true;
@@ -256,7 +274,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_LPS22HB
if (LPS22HB.begin()) {
if (i2cDevicePresent(TELEM_WIRE, 0x5C) && LPS22HB.begin()) {
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
LPS22HB_initialized = true;
} else {
@@ -266,7 +284,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_INA3221
if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_INA3221_ADDRESS) && INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
@@ -281,7 +299,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_INA219
if (INA219.begin(TELEM_WIRE)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_INA219_ADDRESS) && INA219.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
INA219_initialized = true;
} else {
@@ -291,7 +309,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_INA260
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_INA260_ADDRESS) && INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
INA260_initialized = true;
} else {
@@ -301,7 +319,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_INA226
if (INA226.begin()) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_INA226_ADDRESS) && INA226.begin()) {
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS);
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
INA226_initialized = true;
@@ -312,7 +330,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_MLX90614_ADDRESS) && MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
MLX90614_initialized = true;
} else {
@@ -322,7 +340,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_VL53L0X
if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_VL53L0X_ADDRESS) && VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS);
VL53L0X_initialized = true;
} else {
@@ -334,7 +352,7 @@ bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_BMP085
// First argument is MODE (aka oversampling)
// choose ULTRALOWPOWER
if (BMP085.begin(0, TELEM_WIRE)) {
if (i2cDevicePresent(TELEM_WIRE, 0x77) && BMP085.begin(0, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor BMP085");
BMP085_initialized = true;
} else {
@@ -345,7 +363,7 @@ bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_RAK12035
RAK12035.setup(*TELEM_WIRE);
if (RAK12035.begin(TELEM_RAK12035_ADDRESS)) {
if (i2cDevicePresent(TELEM_WIRE, TELEM_RAK12035_ADDRESS) && RAK12035.begin(TELEM_RAK12035_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found sensor RAK12035 at address: %02X", TELEM_RAK12035_ADDRESS);
RAK12035_initialized = true;
} else {
+115 -19
View File
@@ -255,7 +255,12 @@ const char kWebPanelLoginHtml[] PROGMEM = R"HTML(
</section>
</main>
<script>
const TOKEN_KEY = "repeater-token";
const statusEl = document.getElementById("status");
function storeToken(value) {
try { sessionStorage.setItem(TOKEN_KEY, value); } catch (_) {}
try { localStorage.setItem(TOKEN_KEY, value); } catch (_) {}
}
function getPreferredPage() {
const params = new URLSearchParams(window.location.search);
const next = params.get("next");
@@ -269,7 +274,7 @@ const char kWebPanelLoginHtml[] PROGMEM = R"HTML(
statusEl.textContent = text || "Access denied";
return;
}
sessionStorage.setItem("repeater-token", text.trim());
storeToken(text.trim());
window.location.replace(getPreferredPage());
}
document.getElementById("loginBtn").onclick = () => login();
@@ -1021,7 +1026,8 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
</main>
<script>
const RADIO_PRESETS_URL = "https://api.meshcore.nz/api/v1/config";
const isStatsPage = window.location.pathname === "/stats";
let isStatsPage = window.location.pathname === "/stats";
let pageLoadGeneration = 0;
const PANEL_TITLE_KEY = "repeater-panel-title";
const REGION_STATE_CODES = {
"ACT": "act",
@@ -1031,10 +1037,27 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
"Tasmania": "tas",
"Victoria": "vic"
};
let token = sessionStorage.getItem("repeater-token") || "";
const TOKEN_KEY = "repeater-token";
function readStoredToken() {
try {
const sessionToken = sessionStorage.getItem(TOKEN_KEY);
if (sessionToken) return sessionToken;
} catch (_) {}
try {
return localStorage.getItem(TOKEN_KEY) || "";
} catch (_) {
return "";
}
}
function clearStoredToken() {
try { sessionStorage.removeItem(TOKEN_KEY); } catch (_) {}
try { localStorage.removeItem(TOKEN_KEY); } catch (_) {}
}
let token = readStoredToken();
let commandQueue = Promise.resolve();
let radioPresetEntries = [];
let currentRadioConfig = null;
let mqttIataLoaded = false;
const statusEl = document.getElementById("status");
const replyEl = document.getElementById("reply");
const themeToggleEl = document.getElementById("themeToggle");
@@ -1058,7 +1081,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
}
function redirectToLogin() {
const next = isStatsPage ? "/stats" : "/app";
sessionStorage.removeItem("repeater-token");
clearStoredToken();
token = "";
window.location.replace("/?next=" + encodeURIComponent(next));
}
@@ -1084,12 +1107,12 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
if (appBtn) {
appBtn.classList.toggle("active", !isStatsPage);
appBtn.title = "Open app page";
appBtn.onclick = () => window.location.assign("/app");
appBtn.onclick = () => navigateToPage("/app");
}
if (statsBtn) {
statsBtn.classList.toggle("active", isStatsPage);
statsBtn.title = "Open stats page";
statsBtn.onclick = () => window.location.assign("/stats");
statsBtn.onclick = () => navigateToPage("/stats");
}
if (advertBtn) advertBtn.style.display = isStatsPage ? "none" : "";
if (otaBtn) otaBtn.style.display = isStatsPage ? "none" : "";
@@ -2031,6 +2054,12 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
else ctx.lineTo(point.x, point.y);
});
ctx.stroke();
if (coords.length === 1) {
ctx.fillStyle = strokeColor;
ctx.beginPath();
ctx.arc(coords[0].x, coords[0].y, 3.5, 0, Math.PI * 2);
ctx.fill();
}
if (Number.isInteger(hoverIndex) && hoverIndex >= 0 && hoverIndex < coords.length) {
const hover = coords[hoverIndex];
ctx.fillStyle = strokeColor;
@@ -2168,7 +2197,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
const input = document.getElementById("mqttIata");
const banner = document.getElementById("mqttIataBanner");
const inlineWarning = document.getElementById("mqttBrokerWarning");
const showWarning = !!(input && isUnsetMqttIata(input.value));
const showWarning = !!(mqttIataLoaded && input && isUnsetMqttIata(input.value));
if (banner) {
banner.classList.toggle("visible", showWarning && !isStatsPage);
}
@@ -2216,6 +2245,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
const result = await runCommand(prefix + value);
if (!result.ok) return;
if (inputId === "mqttIata") {
mqttIataLoaded = true;
refreshMqttIataWarning();
}
if (inputId === "nodeName") {
@@ -2233,6 +2263,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
}
document.getElementById(inputId).value = value;
if (inputId === "mqttIata") {
mqttIataLoaded = true;
refreshMqttIataWarning();
}
if (inputId === "nodeName") {
@@ -2294,6 +2325,18 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
}
return JSON.parse(text);
});
}
async function validateSession() {
if (!token) {
redirectToLogin();
return false;
}
try {
await fetchJson("/api/session");
return true;
} catch (_) {
return false;
}
}
function getLetsmeshMode() {
const eu = document.getElementById("mqttLetsmeshEu");
@@ -2436,13 +2479,37 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
setRadioPresetStatus(error && error.message ? error.message : "Unable to load community presets.", true);
}
}
async function loadStatsPage() {
function nextPageLoadGeneration() {
pageLoadGeneration++;
return pageLoadGeneration;
}
function isCurrentPageLoad(generation) {
return generation === pageLoadGeneration;
}
function navigateToPage(path, options = {}) {
if (path !== "/app" && path !== "/stats") return;
const replace = options.replace === true;
if (window.location.pathname !== path) {
if (replace) window.history.replaceState({ page:path }, "", path);
else window.history.pushState({ page:path }, "", path);
}
isStatsPage = path === "/stats";
syncNavButton();
refreshMqttIataWarning();
const generation = nextPageLoadGeneration();
initApp(generation);
}
window.addEventListener("popstate", () => {
navigateToPage(window.location.pathname === "/stats" ? "/stats" : "/app", { replace:true });
});
async function loadStatsPage(generation = pageLoadGeneration) {
const summaryEl = document.getElementById("statsSummary");
if (!summaryEl) return;
summaryEl.innerHTML = '<div class="stats-empty">Loading summary...</div>';
let summaryPayload = null;
try {
summaryPayload = await fetchJson("/api/stats?view=summary");
if (!isCurrentPageLoad(generation)) return;
if (!summaryPayload) throw new Error("no summary payload");
renderStatsSummary(summaryPayload);
initTrendCards(getTrendSeriesOrder(summaryPayload));
@@ -2453,8 +2520,10 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
const seriesOrder = getTrendSeriesOrder(summaryPayload);
for (const key of seriesOrder) {
if (!isCurrentPageLoad(generation)) return;
try {
const payload = await fetchJson("/api/stats?series=" + encodeURIComponent(key));
if (!isCurrentPageLoad(generation)) return;
renderTrendResult(key, payload);
} catch (error) {
renderTrendError(key, error && error.message ? error.message : "Unavailable");
@@ -2463,9 +2532,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
}
}
document.getElementById("runBtn").onclick = () => runCommand(document.getElementById("command").value);
const openStats = () => window.location.assign("/stats");
const openApp = () => window.location.assign("/app");
document.getElementById("openStatsPanelBtn").onclick = () => openStats();
document.getElementById("openStatsPanelBtn").onclick = () => navigateToPage("/stats");
document.getElementById("command").addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
@@ -2615,26 +2682,33 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
}
};
document.getElementById("refreshPageBtn").onclick = async () => {
const generation = nextPageLoadGeneration();
if (isStatsPage) {
await loadStatsPage();
await loadStatsPage(generation);
} else {
await initApp();
await initApp(generation);
}
};
document.getElementById("logoutBtn").onclick = () => {
redirectToLogin();
};
async function initApp() {
async function initApp(generation = nextPageLoadGeneration()) {
if (!token) {
redirectToLogin();
return;
}
mqttIataLoaded = false;
refreshMqttIataWarning();
if (!await validateSession()) {
return;
}
showAuthedUi(true);
if (isStatsPage) {
try {
await loadStatsPage();
statusEl.textContent = "Ready";
await loadStatsPage(generation);
if (isCurrentPageLoad(generation)) statusEl.textContent = "Ready";
} catch (error) {
if (!isCurrentPageLoad(generation)) return;
statusEl.textContent = error && error.message ? error.message : "Unable to load stats.";
}
return;
@@ -2647,6 +2721,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
() => loadField("clock", "clockUtc", null, quiet),
() => loadField("get public.key", "publicKey", "uppercase", quiet)
]);
if (!isCurrentPageLoad(generation)) return;
await loadSection("Loading repeater settings...", [
() => loadField("get name", "nodeName", null, quiet),
() => loadField("get lat", "nodeLat", null, quiet),
@@ -2654,17 +2729,20 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
() => loadField("get prv.key", "privateKey", "uppercase", quiet),
() => loadField("get owner.info", "ownerInfo", "multiline", quiet)
]);
if (!isCurrentPageLoad(generation)) return;
await loadSection("Loading radio settings...", [
() => loadRadioConfig(quiet),
() => loadField("get path.hash.mode", "pathHashMode", null, quiet),
() => loadRegionSelection(quiet)
]);
if (!isCurrentPageLoad(generation)) return;
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),
() => loadGhostNodeModeState(quiet)
]);
if (!isCurrentPageLoad(generation)) return;
await loadSection("Loading MQTT settings...", [
() => loadField("get mqtt.iata", "mqttIata", null, quiet),
() => loadField("get mqtt.owner", "mqttOwner", null, quiet),
@@ -2673,18 +2751,19 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
() => loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu", quiet),
() => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet)
]);
if (!isCurrentPageLoad(generation)) return;
statusEl.textContent = "Ready";
} catch (error) {
if (!token) {
if (!token || !isCurrentPageLoad(generation)) {
return;
}
statusEl.textContent = error && error.message ? error.message : "Unable to load repeater settings.";
}
loadRadioPresets();
if (isCurrentPageLoad(generation)) loadRadioPresets();
}
refreshEastmeshModeUi();
refreshLetsmeshModeUi();
initApp();
navigateToPage(isStatsPage ? "/stats" : "/app", { replace:true });
</script>
</body>
</html>
@@ -2737,12 +2816,14 @@ bool WebPanelServer::start() {
httpd_uri_t app_uri = {.uri = "/app", .method = HTTP_GET, .handler = &WebPanelServer::handleApp, .user_ctx = &_route_context};
httpd_uri_t stats_page_uri = {.uri = "/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStatsPage, .user_ctx = &_route_context};
httpd_uri_t login_uri = {.uri = "/login", .method = HTTP_POST, .handler = &WebPanelServer::handleLogin, .user_ctx = &_route_context};
httpd_uri_t session_uri = {.uri = "/api/session", .method = HTTP_GET, .handler = &WebPanelServer::handleSession, .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 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, &stats_page_uri);
httpd_register_uri_handler(_server, &login_uri);
httpd_register_uri_handler(_server, &session_uri);
httpd_register_uri_handler(_server, &command_uri);
httpd_register_uri_handler(_server, &stats_uri);
@@ -2894,6 +2975,21 @@ esp_err_t WebPanelServer::handleLogin(httpd_req_t* req) {
return httpd_resp_sendstr(req, ctx->self->_token);
}
esp_err_t WebPanelServer::handleSession(httpd_req_t* req) {
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
if (ctx == nullptr || ctx->self == 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();
httpd_resp_set_type(req, "application/json; charset=utf-8");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
return httpd_resp_sendstr(req, "{\"authenticated\":true}");
}
esp_err_t WebPanelServer::handleCommand(httpd_req_t* req) {
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {
+1
View File
@@ -65,6 +65,7 @@ private:
static esp_err_t handleApp(httpd_req_t* req);
static esp_err_t handleStatsPage(httpd_req_t* req);
static esp_err_t handleLogin(httpd_req_t* req);
static esp_err_t handleSession(httpd_req_t* req);
static esp_err_t handleCommand(httpd_req_t* req);
static esp_err_t handleStats(httpd_req_t* req);