From 652f1f82ac7109a5c0a69ac0b21b6124b991c099 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 3 Sep 2026 01:59:35 +0000 Subject: [PATCH] Panels show their socket state; editors locked until the doc arrives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ConnNote.vue: a status strip for the WebSocket-backed panels (page and banner editors, analytics view) — "connecting…" while the socket is pending (the staggered slot included), "reconnecting in ~N s…" during backoff, hidden once open. A pending/lost socket no longer reads as a silently empty or stale panel. - PageEditor/BannerEditor: CodeMirror stays non-editable until the page/banner doc has been accepted — typing before it would be clobbered by the accept. A mere disconnect keeps the editor live: text stashes and pending saves flush on reconnect. --- docs/editing.md | 2 +- frontend/src/AnalyticsView.vue | 19 ++++++++++++++++++- frontend/src/BannerEditor.vue | 30 ++++++++++++++++++++++++++++-- frontend/src/ConnNote.vue | 21 +++++++++++++++++++++ frontend/src/PageEditor.vue | 32 ++++++++++++++++++++++++++++++-- 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 frontend/src/ConnNote.vue diff --git a/docs/editing.md b/docs/editing.md index 82ee4c5..f2df883 100644 --- a/docs/editing.md +++ b/docs/editing.md @@ -17,7 +17,7 @@ In-place page re-rendering shared by the banner/site/structure tabs lives in `sw The page and structure tabs share one language selector: `LangSelect.vue` (small flag + dropdown) v-modeled on the shell-wide selection in `editorLang.js` (`''` = primary). While the panel is open that selection overrides the page's normal language preferences: EditorShell calls `swapdoc.setLangOverride`, which pins every `loadPlain` fetch (`?lang=`, the primary by its own code) and pagerite.js's own fetches/prefetches (`pagerite:session-lang`), until the panel closes and the override clears. -All WebSockets (page/banner editors, analytics view, the pagerite.js activity channel) pace their connections through `reconnect.js`: new sockets are created a staggered slot apart (a page load opens Vite's HMR socket plus several of ours at the same moment, and such bursts — like rapid retries — trip the browser's WebSocket throttling, leaving every socket to the host "pending" for minutes), a watchdog closes sockets stuck CONNECTING so they reschedule instead of hanging forever, and retries follow an exponential backoff with jitter that only a healthy connection resets. +All WebSockets (page/banner editors, analytics view, the pagerite.js activity channel) pace their connections through `reconnect.js`: new sockets are created a staggered slot apart (a page load opens Vite's HMR socket plus several of ours at the same moment, and such bursts — like rapid retries — trip the browser's WebSocket throttling, leaving every socket to the host "pending" for minutes), a watchdog closes sockets stuck CONNECTING so they reschedule instead of hanging forever, and retries follow an exponential backoff with jitter that only a healthy connection resets. While a socket is connecting or waiting to reconnect the panel says so (`ConnNote.vue`), and the CodeMirror editors stay locked until their document arrives (typing before the doc accept would be clobbered by it). ## Saving behavior diff --git a/frontend/src/AnalyticsView.vue b/frontend/src/AnalyticsView.vue index ca82644..6cb69e3 100644 --- a/frontend/src/AnalyticsView.vue +++ b/frontend/src/AnalyticsView.vue @@ -28,6 +28,7 @@ import TransitionGraph from './TransitionGraph.vue' import VisitorCharts from './VisitorCharts.vue' import { VIEW_W } from './analytics/chart.js' import { reconnectPolicy, socketSlot, watchConnecting } from './reconnect' +import ConnNote from './ConnNote.vue' // Same centering margin as the charts, so the totals row's left edge // aligns with the chart svg above the natural width. @@ -45,6 +46,16 @@ let connectWatchdog = null const reconnects = reconnectPolicy() let timeInterval = null +// The panel is live data over its socket: while it is connecting or waiting +// to reconnect, say so (ConnNote) instead of showing a silent stale view. +const conn = ref('connecting') // connecting | open | waiting +const retryIn = ref(0) +const connNote = computed(() => + conn.value === 'connecting' ? 'connecting to the server…' + : conn.value === 'waiting' ? `connection lost — reconnecting in ~${retryIn.value} s…` + : '', +) + // The initial range comes from the URL hash (shareable links); without one, // it is derived from the first analytics snapshot: day when the recorded // history is shorter than 24 h, week otherwise. @@ -54,11 +65,13 @@ let rangePinned = Boolean(RANGES[hashRange]) function connectAnalytics() { if (ws) return + conn.value = 'connecting' const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' ws = new WebSocket(`${proto}//${location.host}/_api/ws/analytics`) clearTimeout(connectWatchdog) connectWatchdog = watchConnecting(ws, 'analytics') ws.onopen = () => { + conn.value = 'open' reconnects.opened() error.value = '' } @@ -86,7 +99,10 @@ function connectAnalytics() { // The policy paces the retry: doubling backoff with jitter, reset only // by a healthy connection — a fixed rapid loop trips the browser's // WebSocket throttling (all sockets then sit "pending" for minutes). - reconnectTimeout = setTimeout(connectAnalytics, reconnects.closed()) + const wait = reconnects.closed() + retryIn.value = Math.max(1, Math.round(wait / 1000)) + conn.value = 'waiting' + reconnectTimeout = setTimeout(connectAnalytics, wait) } } @@ -164,6 +180,7 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c +

⚠️ {{ error }}

loading…