Skip to content

fix(editor): say "Session expired" and link back in when the server refuses the socket (QA sweep NET-06) - #31

Merged
NCHILLC merged 1 commit into
mainfrom
fix/qa-net-06-session-expired
Oct 13, 2026
Merged

NCHILLC merged 1 commit into
mainfrom
fix/qa-net-06-session-expired

Conversation

@NCHILLC

@NCHILLC NCHILLC commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Summary

QA sweep finding NET-06 (Minor). With the session cookie cleared, the editor kept looking normal and "Synced", and the next navigation was a silent 302 to /login with no explanation and no way back. The presence socket is now the signal: when it closes without ever opening and the browser is online, the editor asks GET /api/auth/me; a 401 means the session expired, and the mobile pill and desktop footer say "Session expired" and become a link to /login?returnTo=<this page>.

A browser is never told the status of a refused WebSocket handshake (a 401 and a dead network both arrive as close with no open), and the socket's own URL answers 426 before it checks auth, so the probe has to be a separate request.

What changes

  • app/hooks/usePresence.ts: PresenceStatus gains "expired". A 401 from /api/auth/me sets it, anything else clears it, a rejected fetch is ignored, open clears it. Retries continue, so a login in another tab reconnects.
  • app/components/editor/SignInAgainLink.tsx (new), used by MobileSyncPill.tsx (short label) and FooterBar.tsx (full sentence).
  • app/routes/login.tsx + app/lib/session.server.ts: isTokenExpired is exported and the /login loader no longer bounces a token that is past its exp (it used to send it to returnTo, whose loader ended the session and dropped it).
  • Messages editor_session_expired and editor_session_expired_short in en and es-419.
  • docs/develop/fork-log.md: one row (fork deviation, no upstream fix found), with three known ceilings.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Test plan

  • Added: app/hooks/usePresence.test.ts (new), an expired case in MobileSyncPill.test.tsx, a NET-06 case in login-saas-bounce.test.ts.
  • Local, after the final edit: those 3 spec files, 19 tests pass; lint:gates 27 of 27; lint:middleware-budget PASS; build exit 0; pre-commit hook passed.
  • type-check:app was exit 0 before the last edit (one class string and a comment in SignInAgainLink.tsx) and was not re-run after it; this PR's CI covers it.
  • Seen in the browser pane against a scratch D1 at 1024 px and 375 px, dark theme: footer/pill go red, link carries returnTo, logging in lands back on the same item with the typed note intact.
  • Not seen: light theme, a real phone, a __session holding a JWT past its exp through the /login loader (unit spec only).
  • A PR's own CI skips the unit shards and e2e, so the full suite is dispatched on this branch separately.

For the reviewer

  • At 375 px the expired pill leaves the app-bar title 73 px against 121 px when synced ("Foundation" reads "Founda…"). Copy ("log in" vs "sign in") and pill length are open calls.
  • Merge with a normal merge commit, not a squash (see the template note).

🤖 Generated with Claude Code

…efuses the socket (QA sweep NET-06)

A browser is never told the status of a refused WebSocket handshake, so
once the 24-hour token ran out the editor retried under "Reconnecting..."
forever while the next navigation went to a bare /login.

- usePresence gains status "expired": a presence socket that closes
  without having opened asks GET /api/auth/me, and a 401 reads as
  expired. A 200, an unreachable API or isOffline() leaves
  Connecting / Reconnecting. Retries carry on, so logging in from
  another tab reconnects this one.
- MobileSyncPill and FooterBar show that status as SignInAgainLink, an
  anchor to /login?returnTo=<this page with ?section=/?item=>. The
  padding makes the tap target at least 44px tall.
- The /login loader no longer bounces a token past its exp to returnTo
  (isTokenExpired is now exported); that bounce sent the destination's
  loader straight back to a /login with returnTo dropped.
- Copy says "log in" to match the Log In button.

Known ceilings are in the docs/develop/fork-log.md row: a token
invalidated by a password change but not yet past exp is still
bounced; under saas /login redirects to the portal and drops returnTo;
on an origin where the browser refuses the __Host- cookie the readout
says expired.

Chrome: /inspections/<id>/edit — at 1024px with the session ended in a second tab and the server restarted, the footer went red "Session expired: log in to continue" as an underlined link to /login?returnTo=%2Finspections%2F<id>%2Fedit; /api/auth/me answered 401 on each retry
Chrome: /login?returnTo=%2Finspections%2F<id>%2Fedit — following the footer link and logging in landed back on the editor with the footer "Connected" and the note typed while expired present in the Notes box
Chrome: /inspections/<id>/edit?section=structure&item=foundation — at 375px the app-bar pill went red "Session expired" as a link whose returnTo carried section and item, logging in returned to the same Foundation item, and the pill read Synced again
Chrome: /inspections/<id>/edit?section=structure&item=foundation — at 375px the title width fell from 121px (Synced) to 73px (Session expired) so "Foundation" shows as "Founda...", with no horizontal scroll; the pill link measured 38px tall before and 46px after the padding change, the desktop footer link 48px

Not seen in a browser: light theme, a real phone, and the case where
__session still holds a JWT past its exp (covered by the
login-saas-bounce spec only).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@NCHILLC
NCHILLC merged commit 78b8d65 into main Oct 13, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant