diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index efb6532b6..db4878c40 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1438,7 +1438,7 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) { } if (!_webconfig) { _webconfig = new WebConfigServer(&_prefs, _cli.getObserverPrefs(), this, - self_id.pub_key, getFirmwareVer(), getRole(), + self_id.pub_key, getFirmwareVer(), getBuildDate(), getRole(), _cli.getBoard()->getManufacturerName()); } if (force_ap) { diff --git a/scripts/webconfig_mock_server.py b/scripts/webconfig_mock_server.py index d4e004364..3631cdc9a 100644 --- a/scripts/webconfig_mock_server.py +++ b/scripts/webconfig_mock_server.py @@ -187,7 +187,8 @@ class State: # Shaped like build.sh's EMBEDDED_VERSION_STRING # (base[.build][-observer][-channel]-hash) so the console's channel # labelling is exercised against a real version, not "v1.x-mock". - "fw": FW_VERSION, "role": "Repeater", "board": "Heltec V3 (mock)", + "fw": FW_VERSION, "build_date": "6 Jun 2026", + "role": "Repeater", "board": "Heltec V3 (mock)", "uptime_s": int(time.time() - self.start), "runtime_slots": 6, "max_slots": 6, "active_slots": self.active_slots, "max_cmds": CLI_MAX_CMDS, diff --git a/src/helpers/esp32/WebConfigServer.cpp b/src/helpers/esp32/WebConfigServer.cpp index 903036cd7..e2a279dc1 100644 --- a/src/helpers/esp32/WebConfigServer.cpp +++ b/src/helpers/esp32/WebConfigServer.cpp @@ -112,9 +112,10 @@ static portMUX_TYPE s_wc_route_mux = portMUX_INITIALIZER_UNLOCKED; WebConfigServer::WebConfigServer(NodePrefs* prefs, MQTTPrefs* obs, Callbacks* callbacks, const uint8_t* pub_key, const char* fw_ver, + const char* build_date, const char* role, const char* board_name) : _prefs(prefs), _obs(obs), _cb(callbacks), _pub_key(pub_key), - _fw_ver(fw_ver), _role(role), _board_name(board_name) { + _fw_ver(fw_ver), _build_date(build_date), _role(role), _board_name(board_name) { _mux = xSemaphoreCreateMutex(); } @@ -571,6 +572,9 @@ void WebConfigServer::handleStatus(AsyncWebServerRequest* req) { for (int i = 0; i < 8; i++) sprintf(&node_id[i * 2], "%02x", _pub_key[i]); doc["node_id"] = node_id; doc["fw"] = _fw_ver; + // The page shows a trimmed version — base + build number + channel — and + // pairs it with this, the way `ver` does. Both come from the same defines. + doc["build_date"] = _build_date; doc["role"] = _role; doc["board"] = _board_name; doc["uptime_s"] = millis() / 1000; diff --git a/src/helpers/esp32/WebConfigServer.h b/src/helpers/esp32/WebConfigServer.h index 7ef23a92c..e1191fc3e 100644 --- a/src/helpers/esp32/WebConfigServer.h +++ b/src/helpers/esp32/WebConfigServer.h @@ -64,7 +64,7 @@ public: }; WebConfigServer(NodePrefs* prefs, MQTTPrefs* obs, Callbacks* callbacks, - const uint8_t* pub_key, const char* fw_ver, + const uint8_t* pub_key, const char* fw_ver, const char* build_date, const char* role, const char* board_name); ~WebConfigServer(); @@ -125,6 +125,7 @@ private: Callbacks* _cb; const uint8_t* _pub_key; const char* _fw_ver; + const char* _build_date; const char* _role; const char* _board_name; diff --git a/webui/index.html b/webui/index.html index bebdc084f..261d67e46 100644 --- a/webui/index.html +++ b/webui/index.html @@ -582,6 +582,18 @@ function $(s){return document.querySelector(s)} function $$(s){return Array.prototype.slice.call(document.querySelectorAll(s))} function toast(m){var t=$("#toast");t.textContent=m;t.classList.add("show");clearTimeout(t._h);t._h=setTimeout(function(){t.classList.remove("show")},2600)} function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&","<":"<",">":">",'"':"""}[c]})} +// build.sh embeds base[.build][-observer][-channel]-hash, e.g. +// v1.16.0.5-observer-beta-dev-a1b2c3d. What identifies a build to a person is +// the base, the published build number and the channel — the -observer variant +// tag is the same on every observer build and the commit is for machines. `ver` +// still prints the whole string when you want it. +function shortVer(){ + var fw=st.fw||"",m=/^(v?\d+\.\d+\.\d+(?:\.\d+)?)/.exec(fw); + var s=m?m[1]:(fw||"unknown version"); + if(/-dev(-|$)/.test(fw))s+="-dev"; // OTA_CHANNEL_TAG=beta-dev + else if(/-beta(-|$)/.test(fw))s+="-beta"; // a beta-only channel, if ever + return st.build?s+" ("+st.build+")":s; +} function api(path,opts){ opts=opts||{}; @@ -629,9 +641,9 @@ function boot(){ // size for older firmware that doesn't report it. st.mode=s.mode;st.authed=s.auth;st.needsSetup=!!s.needs_setup;st.nslots=s.active_slots||s.runtime_slots||6; if(s.max_cmds>0)CLI_MAX=s.max_cmds; - st.fw=s.fw||"";st.role=s.role||"";st.board=s.board||""; + st.fw=s.fw||"";st.build=s.build_date||"";st.role=s.role||"";st.board=s.board||""; $("#h-name").textContent=s.name||"MeshCore"; - $("#h-sub").textContent=s.role+" · "+s.fw+" · "+s.board; + $("#h-sub").textContent=s.role+" · "+shortVer()+" · "+s.board; var b=$("#h-badge");b.classList.remove("hide"); if(s.mode==="setup"){b.textContent="SETUP";b.classList.add("setup")}else{b.textContent="LAN"} // Only in setup mode: in LAN mode the console is a tab, and the header is @@ -1637,23 +1649,10 @@ function cliEcho(cls,text){ } function cliGap(){var d=cliEcho("gap","");return d} function cliClear(){$("#term-out").innerHTML="";cliBanner();$("#term-in").focus()} -// build.sh composes FIRMWARE_VERSION as base[.build][-observer][-channel]-hash, -// e.g. v1.16.0.5-observer-beta-dev-a1b2c3d. A plain `pio run` never goes through -// build.sh, so it keeps the header default (v1.16.0) with no build number, no -// hash, and — deliberately — no OTA_MANIFEST_BASE, so such a node cannot update -// itself. Worth saying out loud: the version alone does not make that obvious. -function cliChannel(fw){ - if(/-dev(-|$)/.test(fw))return"dev channel"; // OTA_CHANNEL_TAG=beta-dev - if(/-beta(-|$)/.test(fw))return"beta channel"; - if(/^v?\d+\.\d+\.\d+$/.test(fw))return"local build — not from CI, OTA not configured"; - return"release channel"; -} function cliBanner(){ - // Full string, unabbreviated: the 4th component is the published build number - // and the trailing token is the commit, which is what identifies a build. - var ch=cliChannel(st.fw||""); - cliEcho("sys","MeshCore "+(st.fw||"unknown version")+(ch?" ("+ch+")":"")); - cliEcho("sys",(st.role||"")+" · "+(st.board||"")); + cliEcho("sys","MeshCore "+shortVer()); + cliEcho("sys",(st.role||"")+" · "+(st.board||"")+ + (/^v?\d+\.\d+\.\d+$/.test(st.fw||"")?" · local build, OTA not configured":"")); if(st.mode==="setup"){ // The wizard refuses to finish without an admin password; nothing stops a // console-driven setup from rebooting on the factory one, so say so here