From 0f32a8ec8555c44783d8e3859bad66df02d32ee8 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:35:28 -0400 Subject: [PATCH] Fix: hold a long admin toast long enough to read Every toast hid after three seconds, so a multi-line error such as a refused socket proxy address left the screen before it could be read, with no way to bring it back. Derive the timeout from the message length, floored at the previous three seconds and capped at fifteen. --- CHANGELOG.md | 5 +++++ ui/js/admin-logic.js | 7 +++++++ ui/js/admin-shared.js | 4 ++-- ui/test/admin-logic.test.mjs | 9 +++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a55a23..9e7a3a02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- A long error in Admin now stays on screen long enough to read, up to fifteen + seconds. + ## [1.6.0] - 2026-08-13 ### Added diff --git a/ui/js/admin-logic.js b/ui/js/admin-logic.js index a0ca5b37..4662c86f 100644 --- a/ui/js/admin-logic.js +++ b/ui/js/admin-logic.js @@ -227,3 +227,10 @@ export function rejectionLines(rejections, { withName = true } = {}) { export function refusedNoticeKey(count) { return count === 1 ? 'widgetCfg.refused' : 'widgetCfg.refusedPlural'; } + +/* Long errors need longer than short confirmations. 60ms per character is + about 16 characters a second, under a slow reading speed. */ +export function toastMs(message) { + const len = typeof message === 'string' ? message.length : 0; + return Math.min(15000, Math.max(3000, len * 60)); +} diff --git a/ui/js/admin-shared.js b/ui/js/admin-shared.js index d6676732..b8540ad0 100644 --- a/ui/js/admin-shared.js +++ b/ui/js/admin-shared.js @@ -1,5 +1,5 @@ /* Stateless helpers shared by the admin modules. Mutable state stays out. */ -import { nextActiveIndex, recoversSession } from '/js/admin-logic.js?v=056a11e9'; +import { nextActiveIndex, recoversSession, toastMs } from '/js/admin-logic.js?v=056a11e9'; import { el, qa, q } from '/js/utils.js?v=84d58686'; import { t } from '/js/i18n.js?v=133a7aac'; @@ -11,7 +11,7 @@ export const toast = (m, t = 'ok') => { e.textContent = m; e.className = `show ${t}`; clearTimeout(tt); - tt = setTimeout(() => (e.className = ''), 3000); + tt = setTimeout(() => (e.className = ''), toastMs(m)); }; /* Carry `kind` and `detail`, so callers branch on data, never on message diff --git a/ui/test/admin-logic.test.mjs b/ui/test/admin-logic.test.mjs index e6b261f1..ac974b61 100644 --- a/ui/test/admin-logic.test.mjs +++ b/ui/test/admin-logic.test.mjs @@ -16,6 +16,7 @@ import { settingsSaveBlocker, clearsStoredPassword, recoversSession, + toastMs, BLOCK, } from '../js/admin-logic.js'; /* The real strength check, so these assert the rule the save actually applies. */ @@ -485,3 +486,11 @@ test('recoversSession tolerates a missing path', () => { assert.equal(recoversSession(undefined, 401), true); assert.equal(recoversSession(null, 200), false); }); + +test('toastMs holds a long message longer than a short one', () => { + assert.equal(toastMs('Saved'), 3000); + assert.equal(toastMs(''), 3000); + assert.equal(toastMs('x'.repeat(100)), 6000); + assert.equal(toastMs('x'.repeat(1000)), 15000); + assert.equal(toastMs(undefined), 3000); +});