Skip to content

Fix/websocket url and livechat visibility - #177

Merged
ckrew merged 2 commits into
mainfrom
fix/websocket-url-and-livechat-visibility
Aug 28, 2026
Merged

Fix/websocket url and livechat visibility#177
ckrew merged 2 commits into
mainfrom
fix/websocket-url-and-livechat-visibility

Conversation

@ckrew

@ckrew ckrew commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Fix the websocket URL and the Live Chat disappearing act

Two bugs that surfaced together: a browser warning about accessing other apps and services on this device, and a Live Chat widget that appeared to be losing every message.

1. The websocket URL had a host baked into it

REDIS_WS_URL was an absolute ws://localhost:8000/... URL in both env files, and WebSocketContext passed it straight to new WebSocket(). A deployed bundle therefore told every visitor's browser to open a socket to their own machine:

  • Chrome flags it as local network access (the notice that started this).
  • On an HTTPS site, ws:// is mixed content and gets blocked outright.
  • Even when allowed, it connects to the visitor rather than the server, so plugin progress updates and Live Chat silently do nothing in any deployment.

getWebsocketUrl() in services/utilities.js now treats the env value as a path under the app root and resolves the origin and ws:/wss: scheme from window.location at runtime — the same approach getTethysPortalHost() and getTethysAppRoot() already use for every other URL in the frontend. An absolute ws:///wss:// value is still honored as an escape hatch for a separate notification host, and an empty value still disables websockets.

Development now uses the same relative path as production, because the dev server proxies websocket upgrades (ws: true). The proxy context gained "!/ws" so webpack-dev-server's own HMR socket stays local instead of being handed to Django, which has no consumer at that path.

Deployment note: reactapp/config/production.env is gitignored and hand-created per deployment. Each deployment's copy needs REDIS_WS_URL = "visualizations/notifications/ws/" to pick this up.

2. Live Chat scrolled itself off the page

Opening a dashboard with a Live Chat widget scrolled the page down to the message input, leaving the history above the fold — which looked exactly like messages being dropped. Three independent causes:

Flex sizing. ChatLogArea sets flex: 1 1 0% with overflow-y: auto, but a flex item's automatic minimum size is its content size, so it could never shrink below the full height of the message list. The log grew instead of scrolling and pushed the input row out of the grid item and down the page. min-height: 0 lets it shrink so overflow-y actually scrolls inside the tile; box-sizing: border-box keeps PaddedContainer's 16px padding inside the tile height rather than adding to it.

Focus on mount. The autofocus effect fired on mount whenever a username was already cached, and the username input carried autoFocus for the case where one wasn't. Focusing an element scrolls its ancestors to reveal it, so whichever branch rendered, the page jumped to the input on load. Focus now skips the initial mount, passes preventScroll, and moves to whichever input just became active.

Browser scroll restoration. The default history.scrollRestoration re-applies the pre-reload offset — and since dashboard widgets fetch after mount, the page only reaches full height a moment later, so the restore lands after load and reads as the page scrolling itself. index.js sets it to manual.

3. Stale chat history on re-fetch

useState(chatHistory) reads its argument only on mount, so any re-fetch (refresh interval, manual retry, arg change) left the log frozen on the history it mounted with. Added a sync effect that carries over messages which arrived over the websocket after the server took its snapshot, rather than dropping them in the race. Also defaults to [] so an absent chatHistory can't crash the render — propTypes doesn't mark it required.

Verification

The backend was ruled out empirically before any of this was written: the history endpoint returns the full message list, two raw WebSocket clients both receive a broadcast sent by either one, and messages persist correctly. Nothing in controllers.py, visualizations.py, or the channel layer needed changing.

The dev-server proxy change was verified on a throwaway server: HMR socket connects locally (101), the app socket connects through the proxy (101), and a chat frame sent through it round-trips back from Django.

Full Jest suite: 2801 tests across 145 suites, all passing. Nine new regression tests — six for getWebsocketUrl (unset, empty, absolute passthrough, http→ws, https→wss, prefix URLs, trailing-slash host), three for focus behavior (no focus on mount, preventScroll on both later transitions), two for history sync, one for scrollRestoration. eslint and prettier clean.

ckrew and others added 2 commits August 28, 2026 15:27
REDIS_WS_URL was an absolute ws://localhost:8000 URL in both env files, so
a deployed bundle told every visitor's browser to open a socket to their own
machine. Chrome flags that as local network access, HTTPS pages block it as
mixed content, and the socket never reaches the server, so progress updates
and Live Chat silently do nothing outside of local development.

getWebsocketUrl() now treats the env value as a path under the app root and
resolves the origin and ws/wss scheme from window.location at runtime, the
way the rest of the frontend already resolves URLs. An absolute ws:// or
wss:// value is still honored as an escape hatch for a separate notification
host, and an empty value still disables websockets entirely.

Development uses the same relative path as production now that the dev server
proxies websocket upgrades. The proxy context excludes /ws so that webpack's
own HMR socket stays local instead of being handed to Django.

Note that reactapp/config/production.env is gitignored, so each deployment's
copy needs the same relative value to pick this up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opening a dashboard with a Live Chat widget scrolled the page down to the
message input, leaving the chat history above the fold and making the chat
look like it was losing messages. Three separate causes:

ChatLogArea sets flex: 1 1 0% and overflow-y: auto, but a flex item's
automatic minimum size is its content size, so it could never shrink below
the full height of the message list. The log grew instead of scrolling and
pushed the input row out of the grid item and down the page. min-height: 0
lets it shrink so overflow-y actually scrolls inside the tile, and
box-sizing: border-box keeps PaddedContainer's padding inside the tile
height rather than adding to it.

The autofocus effect ran on mount whenever a username was already cached,
and the username input carried autoFocus for the case where one wasn't.
Focusing an element scrolls its ancestors to reveal it, so either branch
yanked the page to the input on load. Focus now skips the initial mount and
passes preventScroll, and it moves to whichever input just became active.

The browser's default scroll restoration re-applied the pre-reload offset
after the widgets' fetches resolved and the page reached full height, which
read as the page scrolling itself a moment after load. index.js sets
history.scrollRestoration to manual so a refresh starts at the top.

Also sync chatHistory into chatLog: useState reads its argument only on
mount, so a re-fetch (refresh interval, manual retry, arg change) left the
log frozen on the history it mounted with. Messages that arrived over the
websocket after the server's snapshot are carried over rather than dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ckrew
ckrew merged commit 13257bf into main Aug 28, 2026
2 checks passed
@ckrew
ckrew deleted the fix/websocket-url-and-livechat-visibility branch August 28, 2026 22:48
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