From 176094d32eaf8e287bcfeb30ecabfc9902a311e9 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:56:46 +0200 Subject: [PATCH] fix(ui): Admin login could hang forever on "Logging in..." AdminScreen's LOGIN phase had no timeout, unlike its COMMAND phase (_cmd_deadline_ms). If a login reply never arrived -- most commonly a saved password gone stale after the remote node's password changed, silently dropped instead of nacked -- the screen stayed stuck with only a manual Cancel to escape. sendRoomLogin() now returns the same est_timeout sendAdminCommand() already exposes; AdminScreen uses it to arm a deadline (poll(), mirroring the COMMAND-phase pattern) that forgets the stale password and returns to the picker on expiry, same as an explicit login rejection already does. Same fix as hotfix/admin-login-timeout (23f43cac), split out of this branch's other in-progress work. Co-Authored-By: Claude Sonnet 5 --- examples/companion_radio/MyMesh.h | 3 +- examples/companion_radio/ui-new/AdminScreen.h | 28 ++++++++++++++++--- .../companion_radio/ui-new/MessagesScreen.h | 4 ++- release-notes.md | 1 + 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index cfbc6e52..8cb83607 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -222,8 +222,7 @@ public: // command source, so this reuses the same sendLogin() the BLE CMD_SEND_LOGIN // path uses; the async result lands in onContactResponse() and is pushed to // the UI via AbstractUITask::onRoomLoginResult(). - bool sendRoomLogin(const ContactInfo& contact, const char* password) { - uint32_t est_timeout; + bool sendRoomLogin(const ContactInfo& contact, const char* password, uint32_t& est_timeout) { if (sendLogin(contact, password, est_timeout) == MSG_SEND_FAILED) return false; clearPendingReqs(); memcpy(&ui_pending_login, contact.id.pub_key, 4); // match this in onContactResponse() diff --git a/examples/companion_radio/ui-new/AdminScreen.h b/examples/companion_radio/ui-new/AdminScreen.h index 6a692f52..9e1c2d91 100644 --- a/examples/companion_radio/ui-new/AdminScreen.h +++ b/examples/companion_radio/ui-new/AdminScreen.h @@ -55,6 +55,12 @@ class AdminScreen : public UIScreen { // was submitted. Distinct from the keyboard being open (kb().render()/ // handleInput() only run while this is false). bool _login_waiting = false; + // Deadline for _login_waiting, mirroring _cmd_deadline_ms/_waiting in the + // COMMAND phase below -- without it, a login reply that never arrives (e.g. + // the remote's password changed since it was saved, and it silently drops + // instead of nacking) leaves the screen stuck on "Logging in..." forever + // with only a manual Cancel to escape. See poll(). + uint32_t _login_deadline_ms = 0; // One-slot memo: the last contact successfully admin-logged-into this visit, // so re-entering COMMAND for the same target right after doesn't require @@ -151,9 +157,10 @@ class AdminScreen : public UIScreen { void startLoginWithSaved(const char* password) { strncpy(_login_pw, password, sizeof(_login_pw) - 1); _login_pw[sizeof(_login_pw) - 1] = '\0'; - bool sent = the_mesh.sendRoomLogin(_target, _login_pw); + uint32_t est_timeout = 0; + bool sent = the_mesh.sendRoomLogin(_target, _login_pw, est_timeout); _task->showAlert(sent ? "Logging in..." : "Login failed", sent ? 1000 : 1500); - if (sent) { _login_waiting = true; _phase = LOGIN; } + if (sent) { _login_waiting = true; _login_deadline_ms = millis() + est_timeout + 4000; _phase = LOGIN; } } void sendCommand() { @@ -410,6 +417,18 @@ public: } void poll() override { + if (_phase == LOGIN && _login_waiting && (int32_t)(millis() - _login_deadline_ms) >= 0) { + _login_waiting = false; + // No response at all is ambiguous (could be a wrong/stale password, could + // just be out of range) -- but a saved password that's gone stale (the + // remote's password changed) is exactly this: silence, not a nack. Treat + // it the same as onRoomLoginResult()'s explicit-failure branch: forget it + // so the next attempt prompts fresh instead of retrying the same dead + // password forever. + the_mesh.forgetRoomPassword(_target.id.pub_key); + _task->showAlert("Login failed (timeout)", 1400); + returnToOrigin(); + } if (_phase == COMMAND && _waiting && (int32_t)(millis() - _cmd_deadline_ms) >= 0) { _waiting = false; if (_fetch_for_edit) { fallBackToBlankEdit(); _task->showAlert("Fetch failed - enter value", 1400); } @@ -488,9 +507,10 @@ public: } else if (r == KeyboardWidget::DONE) { strncpy(_login_pw, kb().buf, sizeof(_login_pw) - 1); _login_pw[sizeof(_login_pw) - 1] = '\0'; - bool sent = the_mesh.sendRoomLogin(_target, _login_pw); + uint32_t est_timeout = 0; + bool sent = the_mesh.sendRoomLogin(_target, _login_pw, est_timeout); _task->showAlert(sent ? "Logging in..." : "Login failed", sent ? 1000 : 1500); - if (sent) _login_waiting = true; else returnToOrigin(); + if (sent) { _login_waiting = true; _login_deadline_ms = millis() + est_timeout + 4000; } else returnToOrigin(); // else: stay in LOGIN until onRoomLoginResult() fires above. } return true; diff --git a/examples/companion_radio/ui-new/MessagesScreen.h b/examples/companion_radio/ui-new/MessagesScreen.h index c24c1058..4d71ba9f 100644 --- a/examples/companion_radio/ui-new/MessagesScreen.h +++ b/examples/companion_radio/ui-new/MessagesScreen.h @@ -722,7 +722,9 @@ public: void startRoomLogin(const char* password) { strncpy(_login_pw, password, sizeof(_login_pw) - 1); _login_pw[sizeof(_login_pw) - 1] = 0; - bool sent = the_mesh.sendRoomLogin(_sel_contact, password); + uint32_t est_timeout = 0; // this screen's login isn't a blocking wait (see + // onRoomLoginResult() below), so no deadline needed + bool sent = the_mesh.sendRoomLogin(_sel_contact, password, est_timeout); _task->showAlert(sent ? "Logging in..." : "Login failed", sent ? 1000 : 1500); } diff --git a/release-notes.md b/release-notes.md index 382e05ef..3f27d1a9 100644 --- a/release-notes.md +++ b/release-notes.md @@ -12,6 +12,7 @@ ### Fixes - **DM/room replies sometimes didn't show who they were addressed to, and room messages could wrap incorrectly.** The reply-prefix (`@[nick] `) was being stripped at the wrong point for room posts and DMs feeding the fullscreen message view, so a DM reply's "To:" header could go missing while a room reply showed the raw `@[nick] ` marker as literal message text; a related mismatch meant the history list's scrollbar/box-height sizing pass wrapped room messages with their sender name still attached, disagreeing with the actual rendered text. +- **Tools › Admin login could get stuck on "Logging in..." forever** if the remote node never sent back a reply — most commonly after its admin/room password was changed, since the on-device UI auto-retries the old saved password with no timeout to fall back on. It now gives up after the same estimated-timeout window the rest of Admin's commands use, forgets the stale saved password, and returns you to the picker to try again with the new one. ---