From c004c4517f188bf2cf0677eb2b0ada649357a63e Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 8 Nov 2025 23:19:51 +0000 Subject: [PATCH] Bulletproof the update auto-refresh with fallback mechanisms (related to #671) After the initial fix, added multiple layers of reliability to ensure updates ALWAYS auto-refresh, even in edge cases: 1. Fallback polling: GlobalUpdateProgressWatcher now polls /api/updates/status every 5 seconds as a safety net in case WebSocket events are dropped, missed, or the tab connects mid-update. This ensures tabs that join late or have WebSocket issues still detect in-progress updates. 2. Manual reload button: Added "Reload Now" button in UpdateProgressModal that appears after 5+ health check attempts during restart. Gives users an escape hatch if auto-reload is delayed (slow DNS, reverse proxy issues, etc.). 3. Already protected: Modal close button only shows when update is complete, preventing users from accidentally closing it mid-update. These changes address all failure modes identified: - Tabs without WebSocket: covered by polling fallback - Tabs joining mid-update: covered by polling fallback - Health check delays: covered by manual reload button - User accidentally closing modal: already prevented The combination of WebSocket events (primary), polling (fallback), health checks (restart detection), and manual reload (escape hatch) should make this bulletproof. --- frontend-modern/src/App.tsx | 36 ++++++++++++++++++- .../src/components/UpdateProgressModal.tsx | 20 ++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 520ce36..ec19174 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -121,8 +121,30 @@ function GlobalUpdateProgressWatcher() { const navigate = useNavigate(); const [showProgressModal, setShowProgressModal] = createSignal(false); const [hasAutoOpened, setHasAutoOpened] = createSignal(false); + let pollInterval: number | undefined; - // Watch for update progress events from WebSocket + // Fallback polling in case WebSocket events are missed + const pollUpdateStatus = async () => { + try { + const status = await UpdatesAPI.getUpdateStatus(); + const inProgress = isUpdateInProgress(status.status); + + if (inProgress && !showProgressModal() && !hasAutoOpened()) { + logger.info('Update in progress detected via polling fallback, showing progress modal', { + status: status.status, + message: status.message, + }); + setShowProgressModal(true); + setHasAutoOpened(true); + } else if (!inProgress && hasAutoOpened()) { + setHasAutoOpened(false); + } + } catch (error) { + // Silently ignore polling errors + } + }; + + // Watch for update progress events from WebSocket (primary mechanism) createEffect(() => { const progress = wsContext?.updateProgress?.() as UpdateStatus | null; @@ -148,6 +170,18 @@ function GlobalUpdateProgressWatcher() { } }); + // Start fallback polling on mount, stop on cleanup + onMount(() => { + // Poll every 5 seconds as a safety net + pollInterval = setInterval(pollUpdateStatus, 5000) as unknown as number; + }); + + onCleanup(() => { + if (pollInterval) { + clearInterval(pollInterval); + } + }); + return ( -
- Pulse is restarting with the new version... - }> - Waiting for Pulse to complete restart. This page will reload automatically. +
+
+ Pulse is restarting with the new version... + }> + Waiting for Pulse to complete restart. This page will reload automatically. + +
+ 5}> +