This commit is contained in:
puzhen 2025-11-20 20:49:07 +08:00
parent 25f4013a78
commit aa3c39afd2
7 changed files with 94 additions and 126 deletions

View file

@ -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

View file

@ -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
})
}
/**

View file

@ -19,6 +19,7 @@ export const CarouselStep: React.FC = () => {
const [currentSlide, setCurrentSlide] = useState(0);
const [isHovered, setIsHovered] = useState(false);
const [api, setApi] = useState<any>(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 (
<div className="flex flex-col gap-lg w-[1120px] max-lg:w-[100%]">
<div className="flex flex-col gap-md ">
@ -136,7 +145,7 @@ export const CarouselStep: React.FC = () => {
</CarouselContent>
</Carousel>
</div>
<div className="flex justify-between items-center gap-sm">
<div className="flex justify-center items-center gap-sm">
<div className="flex justify-center items-center gap-6">
{carouselItems.map((item, index) => (
<div
@ -150,29 +159,6 @@ export const CarouselStep: React.FC = () => {
></div>
))}
</div>
<div className="flex justify-center items-center gap-sm">
<Button
onClick={() => setInitState("done")}
variant="ghost"
size="sm"
>
skip
</Button>
<Button
onClick={() => {
if (currentSlide < carouselItems.length - 1) {
api?.scrollNext(); // not last page, switch to next page
} else {
setInitState("done"); // last page, execute done logic
}
}}
variant="primary"
size="sm"
>
<div>Next</div>
<ArrowRight size={24} className="text-white-100%" />
</Button>
</div>
</div>
</div>
);

View file

@ -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()} */}
<div>
<ProgressInstall
value={isInstalling ? progress : 100}
value={isInstalling || installationState === 'waiting-backend' ? progress : 100}
className="w-full"
/>
<div className="flex items-center gap-2 justify-between">
<div className="text-text-label text-xs font-normal leading-tight ">
{isInstalling ? "System Installing ..." : ""}
{isInstalling ? "System Installing ..." : installationState === 'waiting-backend' ? "Starting backend service..." : ""}
<span className="pl-2">{latestLog?.data}</span>
</div>
<TooltipSimple content={`Cannot retry because state is ${error}`} hidden={true}>
<Button
size="icon"
variant="outline"
className="mt-1"
onClick={retryInstallation}
disabled={isInstalling}
>
<RefreshCcw className="w-4 h-4" />
</Button>
</TooltipSimple>
</div>
</div>
</div>
<div>
{initState === "permissions" && <Permissions />}
{initState === "carousel" && <CarouselStep />}
{initState === "carousel" && installationState !== 'waiting-backend' && <CarouselStep />}
</div>
</div>
{/* error dialog */}

View file

@ -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 (

View file

@ -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);

View file

@ -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<InstallationStoreState>()(
},
],
})),
setWaitingBackend: () =>
set({
state: 'waiting-backend',
progress: 80,
isVisible: true,
}),
retryInstallation: () => {
set({
...initialState,