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.
This commit is contained in:
parent
706822ed58
commit
c004c4517f
2 changed files with 50 additions and 6 deletions
|
|
@ -121,8 +121,30 @@ function GlobalUpdateProgressWatcher() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [showProgressModal, setShowProgressModal] = createSignal(false);
|
const [showProgressModal, setShowProgressModal] = createSignal(false);
|
||||||
const [hasAutoOpened, setHasAutoOpened] = 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(() => {
|
createEffect(() => {
|
||||||
const progress = wsContext?.updateProgress?.() as UpdateStatus | null;
|
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 (
|
return (
|
||||||
<UpdateProgressModal
|
<UpdateProgressModal
|
||||||
isOpen={showProgressModal()}
|
isOpen={showProgressModal()}
|
||||||
|
|
|
||||||
|
|
@ -307,11 +307,21 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
|
||||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="text-sm text-blue-800 dark:text-blue-200">
|
<div class="flex-1">
|
||||||
<Show when={wsDisconnected()} fallback={
|
<div class="text-sm text-blue-800 dark:text-blue-200">
|
||||||
<span>Pulse is restarting with the new version...</span>
|
<Show when={wsDisconnected()} fallback={
|
||||||
}>
|
<span>Pulse is restarting with the new version...</span>
|
||||||
<span>Waiting for Pulse to complete restart. This page will reload automatically.</span>
|
}>
|
||||||
|
<span>Waiting for Pulse to complete restart. This page will reload automatically.</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={wsDisconnected() && healthCheckAttempts > 5}>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
class="mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors"
|
||||||
|
>
|
||||||
|
Reload Now
|
||||||
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue