import { createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js'; import { UpdatesAPI, type UpdateStatus } from '@/api/updates'; interface UpdateProgressModalProps { isOpen: boolean; onClose: () => void; onViewHistory: () => void; connected?: () => boolean; reconnecting?: () => boolean; } export function UpdateProgressModal(props: UpdateProgressModalProps) { const [status, setStatus] = createSignal(null); const [isComplete, setIsComplete] = createSignal(false); const [hasError, setHasError] = createSignal(false); const [isRestarting, setIsRestarting] = createSignal(false); const [wsDisconnected, setWsDisconnected] = createSignal(false); let pollInterval: number | undefined; let healthCheckTimer: number | undefined; let healthCheckAttempts = 0; const clearHealthCheckTimer = () => { if (healthCheckTimer !== undefined) { clearTimeout(healthCheckTimer); healthCheckTimer = undefined; } }; const pollStatus = async () => { try { const currentStatus = await UpdatesAPI.getUpdateStatus(); setStatus(currentStatus); // Check if restarting if (currentStatus.status === 'restarting') { setIsRestarting(true); if (pollInterval) { clearInterval(pollInterval); } // Start health check polling startHealthCheckPolling(); return; } // Check if complete or error if ( currentStatus.status === 'completed' || currentStatus.status === 'idle' || currentStatus.status === 'error' ) { setIsComplete(true); if (currentStatus.status === 'error' || currentStatus.error) { setHasError(true); } if (pollInterval) { clearInterval(pollInterval); } } } catch (error) { console.error('Failed to poll update status:', error); // If we get errors during update, assume we're restarting const currentStatus = status(); const shouldAssumeRestart = !isRestarting() && (!currentStatus || (currentStatus.status !== 'idle' && currentStatus.status !== 'error')); if (shouldAssumeRestart) { if (!currentStatus) { setStatus({ status: 'restarting', progress: 95, message: 'Restarting service...', updatedAt: new Date().toISOString(), }); } setIsRestarting(true); if (pollInterval) { clearInterval(pollInterval); } startHealthCheckPolling(); } } }; const startHealthCheckPolling = () => { clearHealthCheckTimer(); healthCheckAttempts = 0; const checkHealth = async () => { let isHealthy = false; try { const response = await fetch('/api/health', { cache: 'no-store' }); if (response.ok) { isHealthy = true; } } catch (error) { console.warn('Health check request failed, will retry', error); } if (isHealthy) { // Backend is back! Reload the page to get the new version console.log('Backend is healthy again, reloading...'); window.location.reload(); return; } const attempt = Math.min(healthCheckAttempts, 3); const nextDelay = Math.min(2000 * Math.pow(2, attempt), 15000); healthCheckAttempts++; clearHealthCheckTimer(); healthCheckTimer = window.setTimeout(checkHealth, nextDelay); }; // Start checking immediately healthCheckTimer = window.setTimeout(checkHealth, 0); }; // Watch websocket status during restart createEffect(() => { if (!isRestarting()) return; const connected = props.connected?.(); const reconnecting = props.reconnecting?.(); // Track if websocket disconnected during restart if (connected === false && !reconnecting) { setWsDisconnected(true); } // If websocket reconnected after being disconnected, the backend is likely back if (wsDisconnected() && connected === true && !reconnecting) { console.log('WebSocket reconnected after restart, verifying health...'); // Give it a moment for the backend to fully initialize setTimeout(async () => { try { const response = await fetch('/api/health', { cache: 'no-store' }); if (response.ok) { console.log('Backend healthy after websocket reconnect, reloading...'); window.location.reload(); } } catch (_error) { console.warn('Health check failed after websocket reconnect, will keep trying'); } }, 1000); } }); onMount(() => { if (props.isOpen) { // Start polling immediately pollStatus(); // Then poll every 2 seconds pollInterval = setInterval(pollStatus, 2000) as unknown as number; } }); onCleanup(() => { if (pollInterval) { clearInterval(pollInterval); } clearHealthCheckTimer(); }); const getStageIcon = () => { const currentStatus = status(); if (!currentStatus) return null; if (hasError()) { return ( ); } if (isComplete() && !hasError()) { return ( ); } return ( ); }; const getStatusText = () => { const currentStatus = status(); if (isRestarting()) { return 'Pulse is restarting...'; } if (!currentStatus) return 'Initializing...'; if (hasError()) { return 'Update Failed'; } if (isComplete() && !hasError()) { return 'Update Completed Successfully'; } return currentStatus.message || 'Updating...'; }; return (
{/* Header */}

Updating Pulse

{/* Body */}
{/* Icon and Status */}
{getStageIcon()}
{getStatusText()}
{status()!.status.replace('-', ' ')}
{/* Progress Bar */}
Progress {status()!.progress}%
{/* Error Message */}
Error Details:
{status()!.error}
{/* Warning / Info */}
Pulse is restarting with the new version... }> Waiting for Pulse to complete restart. This page will reload automatically.
Please do not close this window or refresh the page during the update.
{/* Footer */}
); }