2 Commits
Author SHA1 Message Date
Daniele 5fb5854ff2 fix(auth): stop the re-login dialog hijacking the login screen
Nightly Build / build (push) Successful in 8m11s
On a cold load with no session, both shells mount every component before
their boot auth check resolves, so a dozen gated /api calls 401 in
parallel and the fetch watch raised the re-login dialog over the login
screen the boot check was about to show (.relogin-backdrop is z-10000,
.login-page z-9999). The user typed their password into the modal, which
only closes itself on success — revealing the login page still up with
the app hidden, so they were asked a second time and only a manual
reload got them in.

The dialog is for a session that dies under an open tab, so gate it on
one having ever been established. Recognising that is passive, in the
same fetch wrapper: mobile.html probes /api/auth/me from a classic
inline script that runs before this module exists, so an explicit marker
per shell would never fire there and the dialog would be dead on mobile.
Any 2xx from a gated endpoint proves a session; only the routes
guard.rs::is_public lets through unauthenticated are excluded.

Knock-on: with the report now a no-op on a cold load, the chat's
reconnect loop no longer stopped on it. Retry only in the native shell,
which authenticates on its own — everywhere else something is already
asking for a password.
2026-08-10 12:23:45 +01:00
dguiducci ff298f1aef fix: renew a session that died under an open tab, instead of eating the message typed into it
Nightly Build / build (push) Successful in 7m34s
Sessions live in the server's RAM, so a restart logs everyone out while the
browser keeps sending a cookie nobody recognises. Nothing noticed: every gated
API call answered 401 into a component that shrugged, and the chat socket was
refused at the upgrade — which reaches `onclose` looking exactly like a flaky
network, so the loop retried every 2 s forever behind "Not connected —
reconnecting, please retry", against a server that would never accept it again.

Retrying was not even the expensive part. `_send()` cleared the composer and
dropped the attachment chips *before* testing the socket, so a long message was
already destroyed by the time the error bubble appeared. The connection test now
comes first and everything below it is unreachable while the socket is down, so
the text stays where the user left it; `/new` and `/clear` move above the guard
because they go over HTTP and reconnect the socket themselves, which is when
they are most wanted.

Detection is one module (`lib/session-expiry.js`) reporting a fact — `auth-expired`,
and `auth-restored` on the way back — with nothing in it that touches the DOM. A
`window.fetch` wrapper flags any 401 from a gated `/api` path, a wrapper rather
than a helper each call site opts into because the components call `fetch`
directly in dozens of places and a seam that must be remembered is one the next
page will forget; `auth/*` and `setup/*` are excluded, where 401 is the normal
answer. The socket's own path asks `probeSession()` before retrying, since it
cannot tell a refusal from a blip. The native mobile shell is guarded inside the
report, so no future caller can reintroduce a web login form there.

The answer is a modal over the page the user is already on, not the login
screen: bouncing to it would throw away everything the page was holding —
including the half-written message this commit exists to save. One password
field, prefilled with the last username this browser logged in as, not
dismissible (with no session nothing on the page works, and a dialog you can
wave away leaves a UI that silently fails every action). On success the chat
reconnects on `auth-restored` and reconciles like any other disconnection.

Known gap: pages that failed a fetch during the outage keep their stale data
until navigated to again. Only the chat re-arms itself.
2026-08-04 13:02:39 +01:00