You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(auth): smoother session-expiry recovery (no toast spam, no focus theft)
When a session expired, every parallel in-flight API call returned
401 and each one independently spawned a "Failed to load X:
Unauthorized" toast — burying the login form under 5-15 stacked
errors. _showLogin() also cloned the form node on every 401, stealing
focus from anyone trying to type their password.
- Toast.muteErrorsForMs(ms): drops error/warning toasts during auth
transitions. Set to 6s on the first 401, self-extends if more arrive.
- App.handleUnauthorized(): now idempotent (_inUnauthState flag) and
destroys the current page so its setInterval polling stops.
- App._showLogin(): if already visible, reuses the existing form
bindings instead of cloning. Best-effort focus restore.
- Auto-focus on #login-user 50ms after _showLogin() so re-auth is
type → tab → type → enter, no mouse needed.
- _inUnauthState cleared in _showApp() so future expirations work.
Also updates whatsnew.js with entries for v7.3.0, v7.3.1, and v7.2.1.
Release: v7.3.1
When a session expired, the UX collapsed: every parallel in-flight API call (containers list, stats, alerts, notifications, host overview…) returned 401 and each one independently:
8
+
9
+
1. Spawned a `Failed to load X: Unauthorized` red toast — burying the login form under 5-15 stacked errors.
10
+
2. Called `App.handleUnauthorized()` → `_showLogin()` → which **cloned the login form node** to remove old listeners, **detaching whatever the user was typing into**. Focus disappeared mid-keystroke. Some users had to triple-click to re-focus the password field.
11
+
3. Did nothing to stop the previous page's `setInterval` polling, so 401s kept arriving every few seconds and the cycle repeated.
12
+
13
+
This release fixes all three.
14
+
15
+
### Fixed
16
+
17
+
-**Toast spam during auth transitions** — added [`Toast.muteErrorsForMs(ms)`](public/js/components/toast.js). When `Api.request` sees a 401, it mutes error/warning toasts for 6s before calling `handleUnauthorized()`. The mute window self-extends if more 401s arrive (so a stuck `setInterval` doesn't break out after 6s).
18
+
-**`App.handleUnauthorized()` is idempotent** — the first 401 transitions to login and sets `_inUnauthState = true`; subsequent 401s are no-ops until login succeeds. Cleared in `_showApp()` so a future expiration triggers fresh.
19
+
-**`App._showLogin()` is idempotent** — if the screen is already visible, the existing form bindings are reused (no clone, no focus theft). Best-effort focus to `#login-user` if nothing else is focused.
20
+
-**Stale polling stopped** — `handleUnauthorized()` now destroys `_currentPage` (calling its `destroy()` to clear `_refreshTimer` / `_statsTimer` / etc.) so the previous page's intervals stop firing while the user is on the login screen.
21
+
-**Auto-focus on login screen** — username field gets focus 50ms after `_showLogin()` so re-auth is `type → tab → type → enter` (no mouse).
-`public/js/api.js` — sets the mute window before calling `handleUnauthorized`, throws `Error` with `isAuthError = true` flag
27
+
-`public/js/app.js` — idempotent `handleUnauthorized` + `_showLogin`, page destroy on 401, auto-focus, `_inUnauthState` cleared in `_showApp`
28
+
-`public/js/pages/whatsnew.js` — entries for v7.3.1, v7.3.0, v7.2.1
29
+
5
30
## [7.3.0] - 2026-04-25 — "Update Notifications"
6
31
7
32
Periodic, opt-out check for new Docker Dash releases on GitHub. Solves the "user cloned the repo a week ago and has no idea v7.3.0 shipped" gap. Designed to be **quiet**: a tiny pulsing ↑ badge next to the sidebar version, click-to-open modal with the full release notes (rendered from the GitHub Release `body`), and a one-click "show upgrade command" for admins.
{type: 'fix',text: 'When the session expired, parallel in-flight API calls each spawned a "Failed to load X: Unauthorized" toast — burying the login form under 5-15 red error toasts. Now: the first 401 mutes error/warning toasts for 6 seconds, so the login form stays clean. The mute window auto-extends if more 401s arrive (e.g. from setIntervals on the previously-active page).'},
18
+
{type: 'fix',text: 'Login form no longer steals keyboard focus mid-typing. Previously, every parallel 401 called _showLogin() which cloned the form node, detaching whatever the user was typing into. Now _showLogin() is idempotent — if the screen is already visible, the form bindings are reused and focus is preserved.'},
19
+
{type: 'fix',text: 'handleUnauthorized() is now idempotent and also destroys the current page (stopping its setInterval timers). Previously, a containers list page kept polling every few seconds while the user was on the login screen, generating a fresh 401 each tick and re-triggering the login dance.'},
20
+
{type: 'improvement',text: 'Username field auto-focuses when the login screen appears, so re-authenticating after a session timeout is now: type → tab → type → enter, with no mouse needed.'},
21
+
],
22
+
},
23
+
{
24
+
version: '7.3.0',
25
+
date: '2026-04-25',
26
+
title: 'In-app update notifications via GitHub releases',
27
+
changes: [
28
+
{type: 'feature',text: 'Subtle pulsing ↑ badge appears next to the sidebar version when a newer Docker Dash release exists on GitHub. Click → modal with the release notes (rendered from this very Release body), publish date, last-checked timestamp, and a "View on GitHub" link.'},
29
+
{type: 'feature',text: 'Admin-only collapsed details inside the modal: copy-pasteable upgrade command (`git pull && APP_VERSION=X.Y.Z docker compose up -d --build app`) with a "back up /data first" reminder. Operators and viewers see the notes but not the command.'},
30
+
{type: 'feature',text: 'New System Settings → General card with a toggle ("Check for updates"), the last-checked timestamp, and a "Check now" button (admin-only). Default ON. Disable for fully air-gapped deployments — zero outbound calls, badge never appears.'},
31
+
{type: 'feature',text: 'Backend: src/services/update-check.js polls api.github.com/repos/<owner>/<repo>/releases/latest every 12 hours (configurable owner/repo via DD_UPDATE_CHECK_OWNER and DD_UPDATE_CHECK_REPO env vars). Cache lives in the settings table. Network failures preserve the existing cache (UI shows last known release until next successful poll).'},
32
+
{type: 'feature',text: 'Endpoints: GET /api/system/update-check (any auth user, for sidebar badge) + POST /api/system/update-check/refresh (admin, force) + POST /api/system/update-check/setting (admin, toggle, audited).'},
33
+
{type: 'improvement',text: 'HA-aware: the 12h cron + the 60s post-boot one-shot both run on the leader replica only, so 4-replica HA still makes 1 GitHub call per 12h (not 4). User-Agent is `docker-dash/<version>` — no install ID, no telemetry beyond what the TCP connection inherently exposes.'},
34
+
{type: 'improvement',text: 'Minimal markdown-to-HTML renderer in update-notifier.js (~190 LOC, no external deps) handles the subset GitHub release notes use: headings, bold/italic, inline code, fenced code blocks, lists, links. All input HTML-escaped first.'},
35
+
{type: 'improvement',text: '24 new tests (semver compare, enable/disable, getStatus state machine including cache JSON corruption tolerance, refresh HTTP behavior with mocked https). Suite: 907 → 931 / 60 suites. Lint clean, npm audit clean.'},
{type: 'fix',text: 'Fixed `TypeError: this._stopLogFollow is not a function` thrown on every navigation away from the containers list view. Root cause was the v6.16.0 lazy-load split: `_stopLogFollow` lives in the lazy-loaded container-detail.js module, but `destroy()` (eager containers.js) called it unconditionally. If the user never opened a detail view, the method didn\'t exist and the call crashed. Now guarded with a `typeof === "function"` check — harmless no-op when the detail module was never loaded.'},
44
+
{type: 'fix',text: 'Sidebar showed `nav.observability` as a raw string (missing translation). Added the key to the `nav:` block in EN + RO; other 9 languages fall back to EN automatically via `_fallback`.'},
0 commit comments