diff --git a/electron/main/index.ts b/electron/main/index.ts index 6abc399be..a720720db 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1301,58 +1301,21 @@ async function createWindow() { }); }); } else { - // Installation is complete - ensure initState is set to 'done' - log.info('Installation already complete - ensuring initState is done'); - - win.webContents.once('dom-ready', () => { - if (!win || win.isDestroyed()) { - log.warn('Window destroyed before DOM ready - skipping localStorage update'); - return; - } - log.info('DOM ready - checking and updating auth-storage to done state'); - win.webContents.executeJavaScript(` - (function() { - try { - const authStorage = localStorage.getItem('auth-storage'); - console.log('[ELECTRON DEBUG] Current auth-storage:', authStorage); - if (authStorage) { - const parsed = JSON.parse(authStorage); - console.log('[ELECTRON DEBUG] Parsed state:', parsed.state); - if (parsed.state && parsed.state.initState !== 'done') { - console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done'); - // Only update the initState field, preserve all other data - const updatedStorage = { - ...parsed, - state: { - ...parsed.state, - initState: 'done' - } - }; - localStorage.setItem('auth-storage', JSON.stringify(updatedStorage)); - console.log('[ELECTRON] initState updated to done, reloading page...'); - return true; // Signal that we need to reload - } else { - console.log('[ELECTRON DEBUG] initState already done or state missing'); - } - } else { - console.log('[ELECTRON DEBUG] No auth-storage found in localStorage'); - } - return false; // No reload needed - } catch (e) { - console.error('[ELECTRON] Failed to update initState:', e); - // Don't modify localStorage if there's an error to prevent data corruption - return false; - } - })(); - `).then(needsReload => { - if (needsReload && win && !win.isDestroyed()) { - log.info('Reloading window after localStorage update'); - win.reload(); - } - }).catch(err => { - log.error('Failed to inject script:', err); - }); - }); + // REMOVED: Previously this block would directly set initState='done' when installation + // was already complete, bypassing the backend readiness check. + // + // This caused a critical bug where: + // 1. Frontend would show immediately (initState='done') + // 2. Backend would still be starting (10-15 seconds) + // 3. Users could interact before backend was ready, causing connection errors + // + // The proper flow is now handled by useInstallationSetup.ts with dual-check mechanism: + // 1. Installation complete event → installationCompleted.current = true + // 2. Backend ready event → backendReady.current = true + // 3. Only when BOTH are true → setInitState('done') + // + // This ensures frontend never shows before backend is ready. + log.info('Installation already complete - letting useInstallationSetup handle state transitions'); } // Load content @@ -1386,6 +1349,10 @@ async function createWindow() { } log.info("[DEPS INSTALL] Dependency Success: ", res.message); + // IMPORTANT: Wait a bit to ensure React components have mounted and registered event listeners + // This prevents race condition where events are sent before listeners are ready + await new Promise(resolve => setTimeout(resolve, 500)); + // IMPORTANT: Always send install-dependencies-complete event when installation check succeeds // This includes both cases: actual installation completed AND installation was skipped (already installed) // The frontend needs this event to properly transition from installation screen to main app diff --git a/electron/main/update.ts b/electron/main/update.ts index ab7a45ac0..a5fc726e6 100644 --- a/electron/main/update.ts +++ b/electron/main/update.ts @@ -49,9 +49,17 @@ export function update(win: Electron.BrowserWindow) { autoUpdater.setFeedURL(feed) if (!app.isPackaged) { console.log('[DEV] setFeedURL:', feed) - autoUpdater.checkForUpdates() + // In development, check for updates but don't fail if it errors + autoUpdater.checkForUpdates().catch(err => { + console.log('[DEV] Update check failed (expected in dev environment):', err.message) + }) } + // Handle errors globally to prevent crashes + autoUpdater.on('error', (error: Error) => { + console.error('[AutoUpdater] Update error:', error.message) + // Don't crash the app on update errors + }) } /** diff --git a/src/components/InstallStep/Carousel.tsx b/src/components/InstallStep/Carousel.tsx index 9fe9e6b5f..0fd0c2224 100644 --- a/src/components/InstallStep/Carousel.tsx +++ b/src/components/InstallStep/Carousel.tsx @@ -19,6 +19,7 @@ export const CarouselStep: React.FC = () => { const [currentSlide, setCurrentSlide] = useState(0); const [isHovered, setIsHovered] = useState(false); const [api, setApi] = useState(null); + const [isDismissed, setIsDismissed] = useState(false); const videoRefs = useRef<(HTMLVideoElement | null)[]>([]); // listen to carousel change useEffect(() => { @@ -93,6 +94,14 @@ export const CarouselStep: React.FC = () => { } } }, [currentSlide, api]); + + // If carousel is dismissed, don't show anything + // The actual transition to 'done' will be handled by useInstallationSetup + // when both installation and backend are ready + if (isDismissed) { + return null; + } + return (
@@ -136,7 +145,7 @@ export const CarouselStep: React.FC = () => {
-
+
{carouselItems.map((item, index) => (
{ >
))}
-
- - -
); diff --git a/src/components/InstallStep/InstallDependencies.tsx b/src/components/InstallStep/InstallDependencies.tsx index 29f91d60f..1a2e8987a 100644 --- a/src/components/InstallStep/InstallDependencies.tsx +++ b/src/components/InstallStep/InstallDependencies.tsx @@ -26,6 +26,7 @@ export const InstallDependencies: React.FC = () => { latestLog, error, isInstalling, + installationState, retryInstallation, exportLog, } = useInstallationUI(); @@ -37,31 +38,20 @@ export const InstallDependencies: React.FC = () => { {/* {isInstalling.toString()} */}
- {isInstalling ? "System Installing ..." : ""} + {isInstalling ? "System Installing ..." : installationState === 'waiting-backend' ? "Starting backend service..." : ""} {latestLog?.data}
-
{initState === "permissions" && } - {initState === "carousel" && } + {initState === "carousel" && installationState !== 'waiting-backend' && }
{/* error dialog */} diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index af4145678..eed4bc7f8 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -82,9 +82,10 @@ const Layout = () => { // Show install screen if either: // 1. The installation store says to show it (isVisible && not completed) // 2. OR if initState is not 'done' (meaning permissions or carousel should show) - const actualShouldShowInstallScreen = shouldShowInstallScreen || initState !== 'done'; + // 3. OR if waiting for backend (installationState === 'waiting-backend') + const actualShouldShowInstallScreen = shouldShowInstallScreen || initState !== 'done' || installationState === 'waiting-backend'; - // Only show main content when installation is complete (initState === 'done') + // Only show main content when installation is complete (initState === 'done' AND not waiting for backend) const shouldShowMainContent = !actualShouldShowInstallScreen; return ( diff --git a/src/hooks/useInstallationSetup.ts b/src/hooks/useInstallationSetup.ts index c4db472f9..782b28332 100644 --- a/src/hooks/useInstallationSetup.ts +++ b/src/hooks/useInstallationSetup.ts @@ -11,9 +11,8 @@ export const useInstallationSetup = () => { // Use ref to track if initial check is done to prevent repeated checks const hasCheckedOnMount = useRef(false); - const isInstalling = useRef(false); // Prevent concurrent installations - // Track installation and backend readiness + // Track installation and backend readiness states const installationCompleted = useRef(false); const backendReady = useRef(false); @@ -23,6 +22,12 @@ export const useInstallationSetup = () => { const addLog = useInstallationStore(state => state.addLog); const setSuccess = useInstallationStore(state => state.setSuccess); const setError = useInstallationStore(state => state.setError); + const setWaitingBackend = useInstallationStore(state => state.setWaitingBackend); + + // REMOVED: Don't reset initState from 'done' to 'carousel' + // Instead, we'll use installationState to control visibility in Layout component + // When tools are already installed, we set installationState to 'waiting-backend' + // which will show progress bar + text without showing carousel slides // Check tool installation status on mount - but only during setup phase useEffect(() => { @@ -37,18 +42,18 @@ export const useInstallationSetup = () => { try { const result = await window.ipcRenderer.invoke("check-tool-installed"); - // Only perform tool check during setup phase (permissions or carousel) - // Once user is in 'done' state (main app), don't check again - // This prevents unexpected navigation away from the main app - if (initState !== 'done') { - if (result.success) { - // REMOVED: Don't automatically set to 'done' even if tools are installed - // We need to wait for proper installation complete + backend ready events - // if (result.isInstalled && initState === "carousel") { - // console.log('[useInstallationSetup] Tools installed but initState is carousel, setting to done'); - // setInitState("done"); - // } + if (result.success) { + // If tools are already installed, mark installation as completed + // This handles the app restart scenario where tools were installed previously + if (result.isInstalled) { + console.log('[useInstallationSetup] Tools already installed, waiting for backend'); + installationCompleted.current = true; + setWaitingBackend(); // Show "waiting for backend" state (progress bar + text, no carousel) + } + // Only perform state transitions during setup phase (permissions or carousel) + // Once user is in 'done' state (main app), don't change initState + if (initState !== 'done') { if (!result.isInstalled && initState === "permissions") { // If tools are NOT installed and we're in permissions state, set to carousel console.log('[useInstallationSetup] Tools not installed and initState is permissions, setting to carousel'); @@ -72,15 +77,12 @@ export const useInstallationSetup = () => { startInstallation(); } else if (initState !== 'done' && toolResult) { // Use the tool result from the previous check to avoid duplicate API calls - if (toolResult.success && !toolResult.isInstalled && !isInstalling.current) { + if (toolResult.success && !toolResult.isInstalled) { console.log('[useInstallationSetup] Tools missing and not installing. Starting installation...'); - isInstalling.current = true; // Set flag to prevent concurrent installations try { await performInstallation(); } catch (installError) { console.error('[useInstallationSetup] Installation failed:', installError); - } finally { - isInstalling.current = false; } } } @@ -101,7 +103,7 @@ export const useInstallationSetup = () => { // Setup Electron IPC listeners (only once) useEffect(() => { - // Helper function to check if both conditions are met + // Helper function to check if both installation and backend are ready const checkAndSetDone = () => { console.log('[useInstallationSetup] Checking readiness - Installation:', installationCompleted.current, 'Backend:', backendReady.current); @@ -113,13 +115,10 @@ export const useInstallationSetup = () => { // Electron IPC event handlers const handleInstallStart = () => { - if (!isInstalling.current) { - isInstalling.current = true; - // Reset states when installation starts - installationCompleted.current = false; - backendReady.current = false; - startInstallation(); - } + // Reset flags when installation starts + installationCompleted.current = false; + backendReady.current = false; + startInstallation(); }; const handleInstallLog = (data: { type: string; data: string }) => { @@ -132,13 +131,16 @@ export const useInstallationSetup = () => { const handleInstallComplete = (data: { success: boolean; code?: number; error?: string }) => { console.log('[useInstallationSetup] Installation complete event received:', data); - isInstalling.current = false; if (data.success) { - setSuccess(); installationCompleted.current = true; console.log('[useInstallationSetup] Installation marked as completed'); - // Check if we can transition to done + + // Don't call setSuccess() yet if we're still waiting for backend + // setSuccess() will be called in handleBackendReady when backend is ready + // This prevents installationState from changing from 'waiting-backend' to 'completed' prematurely + + // Only set initState to done if backend is also ready checkAndSetDone(); } else { setError(data.error || 'Installation failed'); @@ -149,9 +151,14 @@ export const useInstallationSetup = () => { console.log('[useInstallationSetup] Backend ready event received:', data); if (data.success && data.port) { + console.log(`[useInstallationSetup] Backend is ready on port ${data.port}`); backendReady.current = true; - console.log('[useInstallationSetup] Backend marked as ready on port:', data.port); - // Check if we can transition to done + console.log('[useInstallationSetup] Backend marked as ready'); + + // Mark installation as completed (changes state from 'waiting-backend' to 'completed') + setSuccess(); + + // Only set initState to done if installation is also completed checkAndSetDone(); } else { console.error('[useInstallationSetup] Backend failed to start:', data.error); diff --git a/src/store/installationStore.ts b/src/store/installationStore.ts index 1fd4ca6cb..4ea996db3 100644 --- a/src/store/installationStore.ts +++ b/src/store/installationStore.ts @@ -2,11 +2,12 @@ import { create } from 'zustand'; import { subscribeWithSelector } from 'zustand/middleware'; // Define all possible installation states -export type InstallationState = +export type InstallationState = | 'idle' | 'checking-permissions' - | 'showing-carousel' + | 'showing-carousel' | 'installing' + | 'waiting-backend' // New state: tools installed, waiting for backend to be ready | 'error' | 'completed'; @@ -31,6 +32,7 @@ interface InstallationStoreState { addLog: (log: InstallationLog) => void; setSuccess: () => void; setError: (error: string) => void; + setWaitingBackend: () => void; retryInstallation: () => void; completeSetup: () => void; updateProgress: (progress: number) => void; @@ -96,7 +98,14 @@ export const useInstallationStore = create()( }, ], })), - + + setWaitingBackend: () => + set({ + state: 'waiting-backend', + progress: 80, + isVisible: true, + }), + retryInstallation: () => { set({ ...initialState,