Fix/websocket url and livechat visibility - #177
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_URLwas an absolutews://localhost:8000/...URL in both env files, andWebSocketContextpassed it straight tonew WebSocket(). A deployed bundle therefore told every visitor's browser to open a socket to their own machine:ws://is mixed content and gets blocked outright.getWebsocketUrl()inservices/utilities.jsnow treats the env value as a path under the app root and resolves the origin andws:/wss:scheme fromwindow.locationat runtime — the same approachgetTethysPortalHost()andgetTethysAppRoot()already use for every other URL in the frontend. An absolutews:///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.envis gitignored and hand-created per deployment. Each deployment's copy needsREDIS_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.
ChatLogAreasetsflex: 1 1 0%withoverflow-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: 0lets it shrink sooverflow-yactually scrolls inside the tile;box-sizing: border-boxkeepsPaddedContainer'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
autoFocusfor 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, passespreventScroll, and moves to whichever input just became active.Browser scroll restoration. The default
history.scrollRestorationre-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.jssets it tomanual.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 absentchatHistorycan't crash the render —propTypesdoesn'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,preventScrollon both later transitions), two for history sync, one forscrollRestoration. eslint and prettier clean.