diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 70cf9318..259a1900 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -91,6 +91,9 @@ const PORT = parseInt(process.env.PORT || '3008', 10); const HOST = process.env.HOST || '0.0.0.0'; const HOSTNAME = process.env.HOSTNAME || 'localhost'; const DATA_DIR = process.env.DATA_DIR || './data'; +logger.info('[SERVER_STARTUP] process.env.DATA_DIR:', process.env.DATA_DIR); +logger.info('[SERVER_STARTUP] Resolved DATA_DIR:', DATA_DIR); +logger.info('[SERVER_STARTUP] process.cwd():', process.cwd()); const ENABLE_REQUEST_LOGGING_DEFAULT = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true // Runtime-configurable request logging flag (can be changed via settings) diff --git a/apps/server/src/routes/settings/routes/update-global.ts b/apps/server/src/routes/settings/routes/update-global.ts index a04227d8..b45e9965 100644 --- a/apps/server/src/routes/settings/routes/update-global.ts +++ b/apps/server/src/routes/settings/routes/update-global.ts @@ -45,18 +45,24 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) { } // Minimal debug logging to help diagnose accidental wipes. - if ('projects' in updates || 'theme' in updates || 'localStorageMigrated' in updates) { - const projectsLen = Array.isArray((updates as any).projects) - ? (updates as any).projects.length - : undefined; - logger.info( - `Update global settings request: projects=${projectsLen ?? 'n/a'}, theme=${ - (updates as any).theme ?? 'n/a' - }, localStorageMigrated=${(updates as any).localStorageMigrated ?? 'n/a'}` - ); - } + const projectsLen = Array.isArray((updates as any).projects) + ? (updates as any).projects.length + : undefined; + const trashedLen = Array.isArray((updates as any).trashedProjects) + ? (updates as any).trashedProjects.length + : undefined; + logger.info( + `[SERVER_SETTINGS_UPDATE] Request received: projects=${projectsLen ?? 'n/a'}, trashedProjects=${trashedLen ?? 'n/a'}, theme=${ + (updates as any).theme ?? 'n/a' + }, localStorageMigrated=${(updates as any).localStorageMigrated ?? 'n/a'}` + ); + logger.info('[SERVER_SETTINGS_UPDATE] Calling updateGlobalSettings...'); const settings = await settingsService.updateGlobalSettings(updates); + logger.info( + '[SERVER_SETTINGS_UPDATE] Update complete, projects count:', + settings.projects?.length ?? 0 + ); // Apply server log level if it was updated if ('serverLogLevel' in updates && updates.serverLogLevel) { diff --git a/apps/server/src/services/settings-service.ts b/apps/server/src/services/settings-service.ts index e63b075c..8726bba0 100644 --- a/apps/server/src/services/settings-service.ts +++ b/apps/server/src/services/settings-service.ts @@ -273,13 +273,39 @@ export class SettingsService { }; const currentProjectsLen = Array.isArray(current.projects) ? current.projects.length : 0; + // Check if this is a legitimate project removal (moved to trash) vs accidental wipe + const newTrashedProjectsLen = Array.isArray(sanitizedUpdates.trashedProjects) + ? sanitizedUpdates.trashedProjects.length + : Array.isArray(current.trashedProjects) + ? current.trashedProjects.length + : 0; + if ( Array.isArray(sanitizedUpdates.projects) && sanitizedUpdates.projects.length === 0 && currentProjectsLen > 0 ) { - attemptedProjectWipe = true; - delete sanitizedUpdates.projects; + // Only treat as accidental wipe if trashedProjects is also empty + // (If projects are moved to trash, they appear in trashedProjects) + if (newTrashedProjectsLen === 0) { + logger.warn( + '[WIPE_PROTECTION] Attempted to set projects to empty array with no trash! Ignoring update.', + { + currentProjectsLen, + newProjectsLen: 0, + newTrashedProjectsLen, + currentProjects: current.projects?.map((p) => p.name), + } + ); + attemptedProjectWipe = true; + delete sanitizedUpdates.projects; + } else { + logger.info('[LEGITIMATE_REMOVAL] Removing all projects to trash', { + currentProjectsLen, + newProjectsLen: 0, + movedToTrash: newTrashedProjectsLen, + }); + } } ignoreEmptyArrayOverwrite('trashedProjects'); diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index 20824a30..cbe9cb77 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -120,11 +120,14 @@ export function parseLocalStorageSettings(): Partial | null { if (settingsCache) { try { const cached = JSON.parse(settingsCache) as GlobalSettings; - logger.debug('Using fresh settings cache from localStorage'); + const cacheProjectCount = cached?.projects?.length ?? 0; + logger.info(`[CACHE_LOADED] projects=${cacheProjectCount}, theme=${cached?.theme}`); return cached; } catch (e) { logger.warn('Failed to parse settings cache, falling back to old storage'); } + } else { + logger.info('[CACHE_EMPTY] No settings cache found in localStorage'); } // Fall back to old Zustand persisted storage @@ -313,12 +316,19 @@ export async function performSettingsMigration( ): Promise<{ settings: GlobalSettings; migrated: boolean }> { // Get localStorage data const localSettings = parseLocalStorageSettings(); - logger.info(`localStorage has ${localSettings?.projects?.length ?? 0} projects`); - logger.info(`Server has ${serverSettings.projects?.length ?? 0} projects`); + const localProjects = localSettings?.projects?.length ?? 0; + const serverProjects = serverSettings.projects?.length ?? 0; + + logger.info('[MIGRATION_CHECK]', { + localStorageProjects: localProjects, + serverProjects: serverProjects, + localStorageMigrated: serverSettings.localStorageMigrated, + dataSourceMismatch: localProjects !== serverProjects, + }); // Check if migration has already been completed if (serverSettings.localStorageMigrated) { - logger.info('localStorage migration already completed, using server settings only'); + logger.info('[MIGRATION_SKIP] Using server settings only (migration already completed)'); return { settings: serverSettings, migrated: false }; } diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index e1346a91..71c13082 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -172,14 +172,18 @@ export function useSettingsSync(): SettingsSyncState { // Never sync when not authenticated or settings not loaded // The settingsLoaded flag ensures we don't sync default empty state before hydration const auth = useAuthStore.getState(); - logger.debug('syncToServer check:', { + logger.debug('[SYNC_CHECK] Auth state:', { authChecked: auth.authChecked, isAuthenticated: auth.isAuthenticated, settingsLoaded: auth.settingsLoaded, projectsCount: useAppStore.getState().projects?.length ?? 0, }); if (!auth.authChecked || !auth.isAuthenticated || !auth.settingsLoaded) { - logger.debug('Sync skipped: not authenticated or settings not loaded'); + logger.warn('[SYNC_SKIPPED] Not ready:', { + authChecked: auth.authChecked, + isAuthenticated: auth.isAuthenticated, + settingsLoaded: auth.settingsLoaded, + }); return; } @@ -187,7 +191,9 @@ export function useSettingsSync(): SettingsSyncState { const api = getHttpApiClient(); const appState = useAppStore.getState(); - logger.debug('Syncing to server:', { projectsCount: appState.projects?.length ?? 0 }); + logger.info('[SYNC_START] Syncing to server:', { + projectsCount: appState.projects?.length ?? 0, + }); // Build updates object from current state const updates: Record = {}; @@ -204,14 +210,18 @@ export function useSettingsSync(): SettingsSyncState { // Create a hash of the updates to avoid redundant syncs const updateHash = JSON.stringify(updates); if (updateHash === lastSyncedRef.current) { - logger.debug('Sync skipped: no changes'); + logger.debug('[SYNC_SKIP_IDENTICAL] No changes from last sync'); setState((s) => ({ ...s, syncing: false })); return; } - logger.info('Sending settings update:', { projects: updates.projects }); + logger.info('[SYNC_SEND] Sending settings update to server:', { + projects: (updates.projects as any)?.length ?? 0, + trashedProjects: (updates.trashedProjects as any)?.length ?? 0, + }); const result = await api.settings.updateGlobal(updates); + logger.info('[SYNC_RESPONSE] Server response:', { success: result.success }); if (result.success) { lastSyncedRef.current = updateHash; logger.debug('Settings synced to server'); @@ -353,9 +363,11 @@ export function useSettingsSync(): SettingsSyncState { // This is critical - projects list changes must sync right away to prevent loss // when switching between Electron and web modes or closing the app if (newState.projects !== prevState.projects) { - logger.debug('Projects array changed, syncing immediately', { + logger.info('[PROJECTS_CHANGED] Projects array changed, syncing immediately', { prevCount: prevState.projects?.length ?? 0, newCount: newState.projects?.length ?? 0, + prevProjects: prevState.projects?.map((p) => p.name) ?? [], + newProjects: newState.projects?.map((p) => p.name) ?? [], }); syncNow(); return; diff --git a/apps/ui/src/main.ts b/apps/ui/src/main.ts index 3a7e74ca..4d093106 100644 --- a/apps/ui/src/main.ts +++ b/apps/ui/src/main.ts @@ -476,9 +476,14 @@ async function startServer(): Promise { // IMPORTANT: Use shared data directory (not Electron's user data directory) // This ensures Electron and web mode share the same settings/projects - // In dev: project root/data + // In dev: project root/data (navigate from __dirname which is apps/server/dist or apps/ui/dist-electron) // In production: same as Electron user data (for app isolation) - const dataDir = app.isPackaged ? app.getPath('userData') : path.join(__dirname, '../../../data'); + const dataDir = app.isPackaged + ? app.getPath('userData') + : path.join(__dirname, '../../..', 'data'); + logger.info( + `[DATA_DIR] app.isPackaged=${app.isPackaged}, __dirname=${__dirname}, dataDir=${dataDir}` + ); // Build enhanced PATH that includes Node.js directory (cross-platform) const enhancedPath = buildEnhancedPath(command, process.env.PATH || ''); @@ -502,6 +507,7 @@ async function startServer(): Promise { }; logger.info('Server will use port', serverPort); + logger.info('[DATA_DIR_SPAWN] env.DATA_DIR=', env.DATA_DIR); logger.info('Starting backend server...'); logger.info('Server path:', serverPath); @@ -653,20 +659,44 @@ function createWindow(): void { // App lifecycle app.whenReady().then(async () => { - // Ensure userData path is consistent across dev/prod so files land in Automaker dir - try { - const desiredUserDataPath = path.join(app.getPath('appData'), 'Automaker'); - if (app.getPath('userData') !== desiredUserDataPath) { - app.setPath('userData', desiredUserDataPath); - logger.info('userData path set to:', desiredUserDataPath); + // In production, use Automaker dir in appData for app isolation + // In development, use project root for shared data between Electron and web mode + let userDataPathToUse: string; + + if (app.isPackaged) { + // Production: Ensure userData path is consistent so files land in Automaker dir + try { + const desiredUserDataPath = path.join(app.getPath('appData'), 'Automaker'); + if (app.getPath('userData') !== desiredUserDataPath) { + app.setPath('userData', desiredUserDataPath); + logger.info('[PRODUCTION] userData path set to:', desiredUserDataPath); + } + userDataPathToUse = desiredUserDataPath; + } catch (error) { + logger.warn('[PRODUCTION] Failed to set userData path:', (error as Error).message); + userDataPathToUse = app.getPath('userData'); + } + } else { + // Development: Explicitly set userData to project root for shared data between Electron and web + // This OVERRIDES Electron's default userData path (~/.config/Automaker) + // __dirname is apps/ui/dist-electron, so go up to get project root + const projectRoot = path.join(__dirname, '../../..'); + userDataPathToUse = path.join(projectRoot, 'data'); + try { + app.setPath('userData', userDataPathToUse); + logger.info('[DEVELOPMENT] userData path explicitly set to:', userDataPathToUse); + } catch (error) { + logger.warn( + '[DEVELOPMENT] Failed to set userData path, using fallback:', + (error as Error).message + ); + userDataPathToUse = path.join(projectRoot, 'data'); } - } catch (error) { - logger.warn('Failed to set userData path:', (error as Error).message); } // Initialize centralized path helpers for Electron // This must be done before any file operations - setElectronUserDataPath(app.getPath('userData')); + setElectronUserDataPath(userDataPathToUse); // In development mode, allow access to the entire project root (for source files, node_modules, etc.) // In production, only allow access to the built app directory and resources @@ -681,7 +711,12 @@ app.whenReady().then(async () => { // Initialize security settings for path validation // Set DATA_DIR before initializing so it's available for security checks - process.env.DATA_DIR = app.getPath('userData'); + // Use the project's shared data directory in development, userData in production + const mainProcessDataDir = app.isPackaged + ? app.getPath('userData') + : path.join(process.cwd(), 'data'); + process.env.DATA_DIR = mainProcessDataDir; + logger.info('[MAIN_PROCESS_DATA_DIR]', mainProcessDataDir); // ALLOWED_ROOT_DIRECTORY should already be in process.env if set by user // (it will be passed to server process, but we also need it in main process for dialog validation) initAllowedPaths(); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index a23c17c4..ee8ca98a 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -1504,7 +1504,16 @@ export const useAppStore = create()((set, get) => ({ moveProjectToTrash: (projectId) => { const project = get().projects.find((p) => p.id === projectId); - if (!project) return; + if (!project) { + console.warn('[MOVE_TO_TRASH] Project not found:', projectId); + return; + } + + console.log('[MOVE_TO_TRASH] Moving project to trash:', { + projectId, + projectName: project.name, + currentProjectCount: get().projects.length, + }); const remainingProjects = get().projects.filter((p) => p.id !== projectId); const existingTrash = get().trashedProjects.filter((p) => p.id !== projectId); @@ -1517,6 +1526,11 @@ export const useAppStore = create()((set, get) => ({ const isCurrent = get().currentProject?.id === projectId; const nextCurrentProject = isCurrent ? null : get().currentProject; + console.log('[MOVE_TO_TRASH] Updating store with new state:', { + newProjectCount: remainingProjects.length, + newTrashedCount: [trashedProject, ...existingTrash].length, + }); + set({ projects: remainingProjects, trashedProjects: [trashedProject, ...existingTrash], diff --git a/start-automaker.sh b/start-automaker.sh index 86be391c..ef7b1172 100755 --- a/start-automaker.sh +++ b/start-automaker.sh @@ -1075,6 +1075,7 @@ case $MODE in export TEST_PORT="$WEB_PORT" export VITE_SERVER_URL="http://$HOSTNAME:$SERVER_PORT" export PORT="$SERVER_PORT" + export DATA_DIR="$SCRIPT_DIR/data" export CORS_ORIGIN="http://localhost:$WEB_PORT,http://$HOSTNAME:$WEB_PORT,http://127.0.0.1:$WEB_PORT" export VITE_APP_MODE="1"