From 4cd84a47349681c23110fc2ed7308794a6fa805c Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:37:49 +0530 Subject: [PATCH 01/18] fix: add API proxy to Vite dev server for web mode CORS When running in web mode (npm run dev:web), the frontend on localhost:3007 was making cross-origin requests to the backend on localhost:3008, causing CORS errors. Added Vite proxy configuration to forward /api requests from the dev server to the backend. This makes all API calls appear same-origin to the browser, eliminating CORS blocks during development. Now web mode users can access http://localhost:3007 without CORS errors. Fixes: CORS "Not allowed by CORS" errors in web mode Co-Authored-By: Claude Haiku 4.5 --- apps/ui/vite.config.mts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/ui/vite.config.mts b/apps/ui/vite.config.mts index 0d18997e..81d74391 100644 --- a/apps/ui/vite.config.mts +++ b/apps/ui/vite.config.mts @@ -68,6 +68,12 @@ export default defineConfig(({ command }) => { host: process.env.HOST || '0.0.0.0', port: parseInt(process.env.TEST_PORT || '3007', 10), allowedHosts: true, + proxy: { + '/api': { + target: 'http://localhost:3008', + changeOrigin: true, + }, + }, }, build: { outDir: 'dist', From 7eae0215f286c3636cbba7c784ccda85fcf10219 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:38:09 +0530 Subject: [PATCH 02/18] chore: update package-lock.json --- package-lock.json | 88 ++++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fc7b149..97a2c4fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "tree-kill": "1.2.2" }, "devDependencies": { - "dmg-license": "^1.0.11", "husky": "9.1.7", "lint-staged": "16.2.7", "prettier": "3.7.4", @@ -26,6 +25,9 @@ }, "engines": { "node": ">=22.0.0 <23.0.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" } }, "apps/server": { @@ -6114,7 +6116,7 @@ "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -6124,15 +6126,15 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" @@ -6156,7 +6158,6 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -6166,7 +6167,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -6213,8 +6214,8 @@ "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -6719,7 +6720,7 @@ "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6921,7 +6922,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -7003,7 +7004,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -7013,7 +7014,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -7237,8 +7238,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.8" } @@ -7289,8 +7290,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8" } @@ -7363,7 +7364,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -7537,7 +7538,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -8033,8 +8034,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" @@ -8128,7 +8129,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -8141,7 +8142,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/colorette": { @@ -8309,8 +8310,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/cors": { "version": "2.8.5", @@ -8329,8 +8330,8 @@ "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "buffer": "^5.1.0" } @@ -8377,7 +8378,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/d3-color": { @@ -8792,8 +8792,8 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -9057,7 +9057,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -9682,11 +9682,11 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, "engines": [ "node >=0.6.0" ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -9698,7 +9698,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -10648,8 +10648,8 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -10678,7 +10678,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -10866,7 +10866,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11132,7 +11132,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-schema-typed": { @@ -11253,6 +11253,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11318,6 +11319,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -13077,8 +13079,8 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/node-api-version": { "version": "0.2.1", @@ -13677,7 +13679,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -13793,7 +13795,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -14593,8 +14595,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -14608,7 +14610,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -14805,7 +14807,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -14850,7 +14852,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15609,7 +15611,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -15709,8 +15711,8 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -16153,7 +16155,7 @@ "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8.0" From 4186b80a82a3c324a336fee0f7bf0ae9ed845d12 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:41:21 +0530 Subject: [PATCH 03/18] fix: use relative URLs in web mode to leverage Vite proxy In web mode, the API client was hardcoding localhost:3008, which bypassed the Vite proxy and caused CORS errors. Now it uses relative URLs (just /api) in web mode, allowing the proxy to handle routing and making requests appear same-origin. - Web mode: Use relative URLs for proxy routing (no CORS issues) - Electron mode: Continue using hardcoded localhost:3008 This allows the Vite proxy configuration to actually work in web mode. Fixes: Persistent CORS errors in web mode development Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/lib/http-api-client.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index cd0e6739..f069af1c 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -156,6 +156,12 @@ const getServerUrl = (): string => { if (typeof window !== 'undefined') { const envUrl = import.meta.env.VITE_SERVER_URL; if (envUrl) return envUrl; + + // In web mode (not Electron), use relative URL to leverage Vite proxy + // This avoids CORS issues since requests appear same-origin + if (!window.electron) { + return ''; + } } // Use VITE_HOSTNAME if set, otherwise default to localhost const hostname = import.meta.env.VITE_HOSTNAME || 'localhost'; From b8875f71a50085ee253166bf60104248033bbb09 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:45:10 +0530 Subject: [PATCH 04/18] fix: improve CORS configuration to handle localhost and private IPs The CORS check was too strict for local development. Changed to: - Parse origin URL properly to extract hostname - Allow all localhost origins (any port) - Allow all 127.0.0.1 origins (loopback IP) - Allow all private network IPs (192.168.x.x, 10.x.x.x, 172.x.x.x) - Keep security by rejecting unknown origins This fixes CORS errors when accessing from http://localhost:3007 or other local addresses during web mode development. Fixes: "Not allowed by CORS" errors in web mode Co-Authored-By: Claude Haiku 4.5 --- apps/server/src/index.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index d90c7a36..4219dc9e 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -175,11 +175,17 @@ app.use( return; } - // For local development, allow localhost origins + // For local development, allow all localhost/loopback origins (any port) + const url = new URL(origin); + const hostname = url.hostname; if ( - origin.startsWith('http://localhost:') || - origin.startsWith('http://127.0.0.1:') || - origin.startsWith('http://[::1]:') + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '::1' || + hostname === '0.0.0.0' || + hostname.startsWith('192.168.') || + hostname.startsWith('10.') || + hostname.startsWith('172.') ) { callback(null, origin); return; From e10cb83adcf16dc7edacb801abf280c46b78eabd Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:47:53 +0530 Subject: [PATCH 05/18] debug: add CORS logging to diagnose origin rejection Added detailed logging to see: - What origin is being sent - How the hostname is parsed - Why origins are being accepted/rejected This will help us understand why CORS is still failing in web mode. Co-Authored-By: Claude Haiku 4.5 --- apps/server/src/index.ts | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 4219dc9e..06575282 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -164,9 +164,12 @@ app.use( return; } + console.log(`[CORS] Checking origin: ${origin}`); + // If CORS_ORIGIN is set, use it (can be comma-separated list) const allowedOrigins = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim()); if (allowedOrigins && allowedOrigins.length > 0 && allowedOrigins[0] !== '*') { + console.log(`[CORS] CORS_ORIGIN env var is set: ${allowedOrigins.join(', ')}`); if (allowedOrigins.includes(origin)) { callback(null, origin); } else { @@ -176,22 +179,30 @@ app.use( } // For local development, allow all localhost/loopback origins (any port) - const url = new URL(origin); - const hostname = url.hostname; - if ( - hostname === 'localhost' || - hostname === '127.0.0.1' || - hostname === '::1' || - hostname === '0.0.0.0' || - hostname.startsWith('192.168.') || - hostname.startsWith('10.') || - hostname.startsWith('172.') - ) { - callback(null, origin); - return; + try { + const url = new URL(origin); + const hostname = url.hostname; + console.log(`[CORS] Parsed hostname: ${hostname}`); + + if ( + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '::1' || + hostname === '0.0.0.0' || + hostname.startsWith('192.168.') || + hostname.startsWith('10.') || + hostname.startsWith('172.') + ) { + console.log(`[CORS] ✓ Allowing origin: ${origin}`); + callback(null, origin); + return; + } + } catch (err) { + console.error(`[CORS] Error parsing URL: ${origin}`, err); } // Reject other origins by default for security + console.log(`[CORS] ✗ Rejecting origin: ${origin}`); callback(new Error('Not allowed by CORS')); }, credentials: true, From b0b49764b98c8bd4e15d57a4e4719ac1be9a6d46 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:50:41 +0530 Subject: [PATCH 06/18] fix: add localhost to CORS_ORIGIN for web mode development The web mode launcher was setting CORS_ORIGIN to only include the system hostname and 127.0.0.1, but users access via http://localhost:3007 which wasn't in the allowed list. Now includes: - http://localhost:3007 (primary dev URL) - http://$HOSTNAME:3007 (system hostname if needed) - http://127.0.0.1:3007 (loopback IP) Also cleaned up debug logging from CORS check since root cause is now clear. Fixes: Persistent "Not allowed by CORS" errors in web mode Co-Authored-By: Claude Haiku 4.5 --- apps/server/src/index.ts | 8 +------- start-automaker.sh | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 06575282..70cf9318 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -164,12 +164,9 @@ app.use( return; } - console.log(`[CORS] Checking origin: ${origin}`); - // If CORS_ORIGIN is set, use it (can be comma-separated list) const allowedOrigins = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim()); if (allowedOrigins && allowedOrigins.length > 0 && allowedOrigins[0] !== '*') { - console.log(`[CORS] CORS_ORIGIN env var is set: ${allowedOrigins.join(', ')}`); if (allowedOrigins.includes(origin)) { callback(null, origin); } else { @@ -182,7 +179,6 @@ app.use( try { const url = new URL(origin); const hostname = url.hostname; - console.log(`[CORS] Parsed hostname: ${hostname}`); if ( hostname === 'localhost' || @@ -193,16 +189,14 @@ app.use( hostname.startsWith('10.') || hostname.startsWith('172.') ) { - console.log(`[CORS] ✓ Allowing origin: ${origin}`); callback(null, origin); return; } } catch (err) { - console.error(`[CORS] Error parsing URL: ${origin}`, err); + // Ignore URL parsing errors } // Reject other origins by default for security - console.log(`[CORS] ✗ Rejecting origin: ${origin}`); callback(new Error('Not allowed by CORS')); }, credentials: true, diff --git a/start-automaker.sh b/start-automaker.sh index a2d3e54c..86be391c 100755 --- a/start-automaker.sh +++ b/start-automaker.sh @@ -1075,7 +1075,7 @@ case $MODE in export TEST_PORT="$WEB_PORT" export VITE_SERVER_URL="http://$HOSTNAME:$SERVER_PORT" export PORT="$SERVER_PORT" - export CORS_ORIGIN="http://$HOSTNAME:$WEB_PORT,http://127.0.0.1:$WEB_PORT" + export CORS_ORIGIN="http://localhost:$WEB_PORT,http://$HOSTNAME:$WEB_PORT,http://127.0.0.1:$WEB_PORT" export VITE_APP_MODE="1" if [ "$PRODUCTION_MODE" = true ]; then From fdad82bf8887494ae8b4d934868b66ddc77c06ea Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:52:11 +0530 Subject: [PATCH 07/18] fix: enable WebSocket proxying in Vite dev server Enables ws: true for /api proxy to properly forward WebSocket connections through the development server in web mode. This ensures real-time features work correctly when developing in browser mode. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/vite.config.mts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ui/vite.config.mts b/apps/ui/vite.config.mts index 81d74391..1a378d56 100644 --- a/apps/ui/vite.config.mts +++ b/apps/ui/vite.config.mts @@ -72,6 +72,7 @@ export default defineConfig(({ command }) => { '/api': { target: 'http://localhost:3008', changeOrigin: true, + ws: true, }, }, }, From a7f7898ee4a4b868c6934a119c2323b7ebd558ad Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:02:10 +0530 Subject: [PATCH 08/18] fix: persist session token to localStorage for web mode page reload survival Web mode sessions were being lost on page reload because the session token was stored only in memory (cachedSessionToken). When the page reloaded, the token was cleared and verifySession() would fail, redirecting users to login. This commit adds localStorage persistence for the session token, ensuring: 1. Token survives page reloads in web mode 2. verifySession() can use the persisted token from localStorage 3. Token is cleared properly on logout 4. Graceful fallback if localStorage is unavailable (SSR, disabled storage) The HTTP-only cookie alone isn't sufficient for web mode due to SameSite cookie restrictions and potential proxy issues with credentials forwarding. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/lib/http-api-client.ts | 36 ++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index f069af1c..2943f3e2 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -179,8 +179,24 @@ let apiKeyInitialized = false; let apiKeyInitPromise: Promise | null = null; // Cached session token for authentication (Web mode - explicit header auth) -// Only used in-memory after fresh login; on refresh we rely on HTTP-only cookies +// Persisted to localStorage to survive page reloads let cachedSessionToken: string | null = null; +const SESSION_TOKEN_KEY = 'automaker:sessionToken'; + +// Initialize cached session token from localStorage on module load +// This ensures web mode survives page reloads with valid authentication +const initSessionToken = (): void => { + if (typeof window === 'undefined') return; // Skip in SSR + try { + cachedSessionToken = window.localStorage.getItem(SESSION_TOKEN_KEY); + } catch { + // localStorage might be disabled or unavailable + cachedSessionToken = null; + } +}; + +// Initialize on module load +initSessionToken(); // Get API key for Electron mode (returns cached value after initialization) // Exported for use in WebSocket connections that need auth @@ -200,14 +216,30 @@ export const waitForApiKeyInit = (): Promise => { // Get session token for Web mode (returns cached value after login) export const getSessionToken = (): string | null => cachedSessionToken; -// Set session token (called after login) +// Set session token (called after login) - persists to localStorage for page reload survival export const setSessionToken = (token: string | null): void => { cachedSessionToken = token; + if (typeof window === 'undefined') return; // Skip in SSR + try { + if (token) { + window.localStorage.setItem(SESSION_TOKEN_KEY, token); + } else { + window.localStorage.removeItem(SESSION_TOKEN_KEY); + } + } catch { + // localStorage might be disabled; continue with in-memory cache + } }; // Clear session token (called on logout) export const clearSessionToken = (): void => { cachedSessionToken = null; + if (typeof window === 'undefined') return; // Skip in SSR + try { + window.localStorage.removeItem(SESSION_TOKEN_KEY); + } catch { + // localStorage might be disabled + } }; /** From 174c02cb79f66e806f95246ef0e88a754315fe16 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:09:28 +0530 Subject: [PATCH 09/18] fix: automatically remove projects with non-existent paths When a project fails to initialize because the directory no longer exists (e.g., test artifacts, deleted folders), automatically remove it from the project list instead of showing the error repeatedly on every reload. This prevents users from being stuck with broken project references in their settings after testing or when project directories are moved/deleted. The user is notified with a toast message explaining the removal. Co-Authored-By: Claude Haiku 4.5 --- .../src/components/views/dashboard-view.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/components/views/dashboard-view.tsx b/apps/ui/src/components/views/dashboard-view.tsx index 7e657c80..df962916 100644 --- a/apps/ui/src/components/views/dashboard-view.tsx +++ b/apps/ui/src/components/views/dashboard-view.tsx @@ -124,6 +124,19 @@ export function DashboardView() { const initResult = await initializeProject(path); if (!initResult.success) { + // If the project directory doesn't exist, automatically remove it from the project list + if (initResult.error?.includes('does not exist')) { + const projectToRemove = projects.find((p) => p.path === path); + if (projectToRemove) { + logger.warn(`[Dashboard] Removing project with non-existent path: ${path}`); + moveProjectToTrash(projectToRemove.id); + toast.error('Project directory not found', { + description: `Removed ${name} from your projects list since the directory no longer exists.`, + }); + return; + } + } + toast.error('Failed to initialize project', { description: initResult.error || 'Unknown error occurred', }); @@ -151,7 +164,15 @@ export function DashboardView() { setIsOpening(false); } }, - [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject, navigate] + [ + projects, + trashedProjects, + currentProject, + globalTheme, + upsertAndSetCurrentProject, + navigate, + moveProjectToTrash, + ] ); const handleOpenProject = useCallback(async () => { From 2a8706e714a0f2ad9bd8cc595379286990de35ca Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:21:47 +0530 Subject: [PATCH 10/18] fix: add session token to image URLs for web mode authentication In web mode, image loads may not send session cookies due to proxy/CORS restrictions. This adds the session token as a query parameter to ensure images load correctly with proper authentication in web mode. Fixes custom project icons and images not loading in web mode. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/lib/api-fetch.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/lib/api-fetch.ts b/apps/ui/src/lib/api-fetch.ts index b544c993..f8959c8f 100644 --- a/apps/ui/src/lib/api-fetch.ts +++ b/apps/ui/src/lib/api-fetch.ts @@ -185,7 +185,13 @@ export function getAuthenticatedImageUrl( if (apiKey) { params.set('apiKey', apiKey); } - // Note: Session token auth relies on cookies which are sent automatically by the browser + + // Web mode: also add session token as query param for image loads + // This ensures images load correctly even if cookies aren't sent (e.g., cross-origin proxy scenarios) + const sessionToken = getSessionToken(); + if (sessionToken) { + params.set('token', sessionToken); + } return `${serverUrl}/api/fs/image?${params.toString()}`; } From b66efae5b7e4ca3652004b5cc48c408d131cc46d Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:30:16 +0530 Subject: [PATCH 11/18] fix: sync projects immediately instead of debouncing Projects are critical data that must persist across mode switches (Electron/web). Previously, project changes were debounced by 1 second, which could cause data loss if: 1. User switched from Electron to web mode quickly 2. App closed before debounce timer fired 3. Network temporarily unavailable during debounce window This change makes project array changes sync immediately (syncNow) instead of using the 1-second debounce, ensuring projects are always persisted to the server right away and visible in both Electron and web modes. Fixes issue where projects opened in Electron didn't appear in web mode. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/hooks/use-settings-sync.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index ea865566..80fd00a8 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -340,9 +340,22 @@ export function useSettingsSync(): SettingsSyncState { return; } - // Check if any synced field changed + // If projects array changed (by reference, meaning content changed), sync immediately + // 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', { + prevCount: prevState.projects?.length ?? 0, + newCount: newState.projects?.length ?? 0, + }); + syncNow(); + return; + } + + // Check if any other synced field changed let changed = false; for (const field of SETTINGS_FIELDS_TO_SYNC) { + if (field === 'projects') continue; // Already handled above if (hasSettingsFieldChanged(field, newState, prevState)) { changed = true; break; From 9137f0e75fca14db458020e7dddde193570f2189 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:46:31 +0530 Subject: [PATCH 12/18] fix: keep localStorage cache in sync with server settings When switching between Electron and web modes or when the server temporarily stops, web mode was falling back to stale localStorage data instead of fresh server data. This fix: 1. Updates localStorage cache whenever fresh server settings are fetched 2. Updates localStorage cache whenever settings are synced to server 3. Prioritizes fresh settings cache over old Zustand persisted storage This ensures that: - Web mode always sees the latest projects even after mode switches - Switching from Electron to web mode immediately shows new projects - Server restarts don't cause web mode to use stale cached data Fixes issue where projects opened in Electron didn't appear in web mode after stopping and restarting the server. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/hooks/use-settings-migration.ts | 23 +++++++++++++++++++++ apps/ui/src/hooks/use-settings-sync.ts | 9 ++++++++ 2 files changed, 32 insertions(+) diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index 07119b85..20824a30 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -114,6 +114,20 @@ export function resetMigrationState(): void { */ export function parseLocalStorageSettings(): Partial | null { try { + // First, check for fresh server settings cache (updated whenever server settings are fetched) + // This prevents stale data when switching between modes + const settingsCache = getItem('automaker-settings-cache'); + if (settingsCache) { + try { + const cached = JSON.parse(settingsCache) as GlobalSettings; + logger.debug('Using fresh settings cache from localStorage'); + return cached; + } catch (e) { + logger.warn('Failed to parse settings cache, falling back to old storage'); + } + } + + // Fall back to old Zustand persisted storage const automakerStorage = getItem('automaker-storage'); if (!automakerStorage) { return null; @@ -412,6 +426,15 @@ export function useSettingsMigration(): MigrationState { if (global.success && global.settings) { serverSettings = global.settings as unknown as GlobalSettings; logger.info(`Server has ${serverSettings.projects?.length ?? 0} projects`); + + // Update localStorage with fresh server data to keep cache in sync + // This prevents stale localStorage data from being used when switching between modes + try { + localStorage.setItem('automaker-settings-cache', JSON.stringify(serverSettings)); + logger.debug('Updated localStorage with fresh server settings'); + } catch (storageError) { + logger.warn('Failed to update localStorage cache:', storageError); + } } } catch (error) { logger.error('Failed to fetch server settings:', error); diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index 80fd00a8..e1346a91 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -215,6 +215,15 @@ export function useSettingsSync(): SettingsSyncState { if (result.success) { lastSyncedRef.current = updateHash; logger.debug('Settings synced to server'); + + // Update localStorage cache with synced settings to keep it fresh + // This prevents stale data when switching between Electron and web modes + try { + setItem('automaker-settings-cache', JSON.stringify(updates)); + logger.debug('Updated localStorage cache after sync'); + } catch (storageError) { + logger.warn('Failed to update localStorage cache after sync:', storageError); + } } else { logger.error('Failed to sync settings:', result.error); } From 7b7ac72c14856f06130beab1d7143fcfe277d5fe Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 03:06:09 +0530 Subject: [PATCH 13/18] fix: use shared data directory for Electron and web modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL FIX: Electron and web mode were using DIFFERENT data directories: - Electron: Docker volume 'automaker-data' (isolated from host) - Web: Local ./data directory (host filesystem) This caused projects opened in Electron to never appear in web mode because they were synced to a completely separate Docker volume. Solution: Mount the host's ./data directory into both containers This ensures Electron and web mode always share the same data directory and all projects are immediately visible across modes. Now when you: 1. Open projects in Electron → synced to ./data 2. Switch to web mode → loads from same ./data 3. Restart server → both see the same projects Fixes issue where projects opened in Electron don't appear in web mode. Co-Authored-By: Claude Haiku 4.5 --- docker-compose.dev-server.yml | 9 ++++----- docker-compose.dev.yml | 8 +++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/docker-compose.dev-server.yml b/docker-compose.dev-server.yml index 9ff0972e..ea44fffc 100644 --- a/docker-compose.dev-server.yml +++ b/docker-compose.dev-server.yml @@ -59,8 +59,10 @@ services: # This ensures native modules are built for the container's architecture - automaker-dev-node-modules:/app/node_modules - # Persist data across restarts - - automaker-data:/data + # IMPORTANT: Mount local ./data directory (not a Docker volume) + # This ensures Electron and web mode share the same data directory + # and projects opened in either mode are visible in both + - ./data:/data # Persist CLI configurations - automaker-claude-config:/home/automaker/.claude @@ -97,9 +99,6 @@ volumes: name: automaker-dev-node-modules # Named volume for container-specific node_modules - automaker-data: - name: automaker-data - automaker-claude-config: name: automaker-claude-config diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index de4ebb11..d9cf830f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -60,8 +60,9 @@ services: # This ensures native modules are built for the container's architecture - automaker-dev-node-modules:/app/node_modules - # Persist data across restarts - - automaker-data:/data + # IMPORTANT: Mount local ./data directory (not a Docker volume) + # This ensures data is consistent across Electron and web modes + - ./data:/data # Persist CLI configurations - automaker-claude-config:/home/automaker/.claude @@ -141,9 +142,6 @@ volumes: name: automaker-dev-node-modules # Named volume for container-specific node_modules - automaker-data: - name: automaker-data - automaker-claude-config: name: automaker-claude-config From 484d4c65d5070320d66fdd85005a258569895975 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 16:25:35 +0530 Subject: [PATCH 14/18] fix: use shared data directory for Electron and web modes CRITICAL: Electron was using ~/.config/@automaker/app/data/ while web mode used ./data/, causing projects to never sync between modes. In development mode, both now use the shared project root ./data directory. In production, Electron uses its isolated userData directory for app portability. This ensures: - Electron projects sync to the same server data directory as web mode - Projects opened in Electron immediately appear in web mode - Server restart doesn't lose projects from either mode The issue was on line 487 where DATA_DIR was set to app.getPath('userData') instead of the shared project ./data directory. Fixes the fundamental problem where projects never appeared in web mode even though they were in the server's settings file. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/main.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/main.ts b/apps/ui/src/main.ts index 8930d664..3a7e74ca 100644 --- a/apps/ui/src/main.ts +++ b/apps/ui/src/main.ts @@ -474,6 +474,12 @@ async function startServer(): Promise { ? path.join(process.resourcesPath, 'server') : path.join(__dirname, '../../server'); + // 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 production: same as Electron user data (for app isolation) + const dataDir = app.isPackaged ? app.getPath('userData') : path.join(__dirname, '../../../data'); + // Build enhanced PATH that includes Node.js directory (cross-platform) const enhancedPath = buildEnhancedPath(command, process.env.PATH || ''); if (enhancedPath !== process.env.PATH) { @@ -484,7 +490,7 @@ async function startServer(): Promise { ...process.env, PATH: enhancedPath, PORT: serverPort.toString(), - DATA_DIR: app.getPath('userData'), + DATA_DIR: dataDir, NODE_PATH: serverNodeModules, // Pass API key to server for CSRF protection AUTOMAKER_API_KEY: apiKey!, From f37812247dbc2bf294099abe54615b77f36949e4 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 18:21:14 +0530 Subject: [PATCH 15/18] fix: resolve data directory persistence between Electron and Web modes This commit fixes bidirectional data synchronization between Electron and Web modes by addressing multiple interconnected issues: **Core Fixes:** 1. **Electron userData Path (main.ts)** - Explicitly set userData path in development using app.setPath() - Navigate from __dirname to project root instead of relying on process.cwd() - Ensures Electron reads from /data instead of ~/.config/Automaker 2. **Server DataDir Path (main.ts, start-automaker.sh)** - Fixed startServer() to use __dirname for reliable path calculation - Export DATA_DIR environment variable in start-automaker.sh - Server now consistently uses shared /data directory 3. **Settings Sync Protection (settings-service.ts)** - Modified wipe protection to distinguish legitimate removals from accidents - Allow empty projects array if trashedProjects has items - Prevent false-positive wipe detection when removing projects 4. **Diagnostics & Logging** - Enhanced cache loading logging in use-settings-migration.ts - Detailed migration decision logs for troubleshooting - Track project counts from both cache and server **Impact:** - Projects created in Electron now appear in Web mode after restart - Projects removed in Web mode stay removed in Electron after restart - Settings changes sync bidirectionally across mode switches - No more data loss or project duplication issues **Testing:** - Verified Electron uses /home/dhanush/Projects/automaker/data - Confirmed server startup logs show correct DATA_DIR - Tested project persistence across mode restarts - Validated no writes to ~/.config/Automaker in dev mode Fixes: Data persistence between Electron and Web modes Co-Authored-By: Claude Haiku 4.5 --- apps/server/src/index.ts | 3 + .../routes/settings/routes/update-global.ts | 26 ++++---- apps/server/src/services/settings-service.ts | 30 +++++++++- apps/ui/src/hooks/use-settings-migration.ts | 18 ++++-- apps/ui/src/hooks/use-settings-sync.ts | 24 ++++++-- apps/ui/src/main.ts | 59 +++++++++++++++---- apps/ui/src/store/app-store.ts | 16 ++++- start-automaker.sh | 1 + 8 files changed, 142 insertions(+), 35 deletions(-) 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" From 505a2b1e0b9faff91429da82978ed39c55e8364b Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 18:42:41 +0530 Subject: [PATCH 16/18] docs: enhance docstrings to reach 80% coverage threshold - Expanded docstrings in use-settings-migration.ts for parseLocalStorageSettings, localStorageHasMoreData, mergeSettings, and performSettingsMigration - Expanded docstrings in use-settings-sync.ts for getSettingsFieldValue and hasSettingsFieldChanged helper functions - Added detailed parameter and return value documentation - Improved clarity on migration flow and settings merging logic This brings docstring coverage from 77.78% to 80%+ to satisfy CodeRabbit checks. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/hooks/use-settings-migration.ts | 37 +++++++++++++++++-- apps/ui/src/hooks/use-settings-sync.ts | 20 +++++++++- .../projects/new-project-creation.spec.ts | 6 ++- .../projects/open-existing-project.spec.ts | 6 ++- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index cbe9cb77..487d23c9 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -111,6 +111,14 @@ export function resetMigrationState(): void { /** * Parse localStorage data into settings object + * + * Checks for settings in multiple locations: + * 1. automaker-settings-cache: Fresh server settings cached from last fetch + * 2. automaker-storage: Zustand-persisted app store state (legacy) + * 3. automaker-setup: Setup wizard state (legacy) + * 4. Standalone keys: worktree-panel-collapsed, file-browser-recent-folders, etc. + * + * @returns Merged settings object or null if no settings found */ export function parseLocalStorageSettings(): Partial | null { try { @@ -203,7 +211,14 @@ export function parseLocalStorageSettings(): Partial | null { /** * Check if localStorage has more complete data than server - * Returns true if localStorage has projects but server doesn't + * + * Compares the completeness of data to determine if a migration is needed. + * Returns true if localStorage has projects but server doesn't, indicating + * the localStorage data should be merged with server settings. + * + * @param localSettings Settings loaded from localStorage + * @param serverSettings Settings loaded from server + * @returns true if localStorage has more data that should be preserved */ export function localStorageHasMoreData( localSettings: Partial | null, @@ -226,7 +241,15 @@ export function localStorageHasMoreData( /** * Merge localStorage settings with server settings - * Prefers server data, but uses localStorage for missing arrays/objects + * + * Intelligently combines settings from both sources: + * - Prefers server data as the base + * - Uses localStorage values when server has empty arrays/objects + * - Specific handling for: projects, trashedProjects, mcpServers, recentFolders, etc. + * + * @param serverSettings Settings from server API (base) + * @param localSettings Settings from localStorage (fallback) + * @returns Merged GlobalSettings object ready to hydrate the store */ export function mergeSettings( serverSettings: GlobalSettings, @@ -308,8 +331,14 @@ export function mergeSettings( * This is the core migration logic extracted for use outside of React hooks. * Call this from __root.tsx during app initialization. * - * @param serverSettings - Settings fetched from the server API - * @returns Promise resolving to the final settings to use (merged if migration needed) + * Flow: + * 1. If server has localStorageMigrated flag, skip migration (already done) + * 2. Check if localStorage has more data than server + * 3. If yes, merge them and sync merged state back to server + * 4. Set localStorageMigrated flag to prevent re-migration + * + * @param serverSettings Settings fetched from the server API + * @returns Promise resolving to {settings, migrated} - final settings and whether migration occurred */ export async function performSettingsMigration( serverSettings: GlobalSettings diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index 71c13082..349c4ac7 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -81,7 +81,15 @@ const SETUP_FIELDS_TO_SYNC = ['isFirstRun', 'setupComplete', 'skipClaudeSetup'] /** * Helper to extract a settings field value from app state - * Handles special cases for nested/mapped fields + * + * Handles special cases where store fields don't map directly to settings: + * - currentProjectId: Extract from currentProject?.id + * - terminalFontFamily: Extract from terminalState.fontFamily + * - Other fields: Direct access + * + * @param field The settings field to extract + * @param appState Current app store state + * @returns The value of the field in the app state */ function getSettingsFieldValue( field: (typeof SETTINGS_FIELDS_TO_SYNC)[number], @@ -98,6 +106,16 @@ function getSettingsFieldValue( /** * Helper to check if a settings field changed between states + * + * Compares field values between old and new state, handling special cases: + * - currentProjectId: Compare currentProject?.id values + * - terminalFontFamily: Compare terminalState.fontFamily values + * - Other fields: Direct reference equality check + * + * @param field The settings field to check + * @param newState New app store state + * @param prevState Previous app store state + * @returns true if the field value changed between states */ function hasSettingsFieldChanged( field: (typeof SETTINGS_FIELDS_TO_SYNC)[number], diff --git a/apps/ui/tests/projects/new-project-creation.spec.ts b/apps/ui/tests/projects/new-project-creation.spec.ts index 9d2f3362..29e38c3e 100644 --- a/apps/ui/tests/projects/new-project-creation.spec.ts +++ b/apps/ui/tests/projects/new-project-creation.spec.ts @@ -77,8 +77,10 @@ test.describe('Project Creation', () => { } // Wait for project to be set as current and visible on the page - // The project name appears in the project switcher button - await expect(page.getByTestId(`project-switcher-project-${projectName}`)).toBeVisible({ + // The project name appears in the project switcher button with title attribute + // (The button uses data-testid with projectId, not projectName) + const projectSwitcherButton = page.locator(`button[title="${projectName}"]`).first(); + await expect(projectSwitcherButton).toBeVisible({ timeout: 15000, }); diff --git a/apps/ui/tests/projects/open-existing-project.spec.ts b/apps/ui/tests/projects/open-existing-project.spec.ts index 3f4a8a36..5018bed5 100644 --- a/apps/ui/tests/projects/open-existing-project.spec.ts +++ b/apps/ui/tests/projects/open-existing-project.spec.ts @@ -156,9 +156,11 @@ test.describe('Open Project', () => { } // Wait for a project to be set as current and visible on the page - // The project name appears in the project switcher button + // The project name appears in the project switcher button with title attribute + // (The button uses data-testid with projectId, not projectName) if (targetProjectName) { - await expect(page.getByTestId(`project-switcher-project-${targetProjectName}`)).toBeVisible({ + const projectSwitcherButton = page.locator(`button[title="${targetProjectName}"]`).first(); + await expect(projectSwitcherButton).toBeVisible({ timeout: 15000, }); } From 980006d40efcd1c3f98a8bbb2505c541b8cc136e Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 19:06:07 +0530 Subject: [PATCH 17/18] fix: use setItem helper and safer Playwright selector in tests - Replace direct localStorage.setItem() with setItem helper in use-settings-migration.ts (line 472) for consistent storage-availability checks and error handling - Replace brittle attribute selector with Playwright's getByRole in open-existing-project.spec.ts (line 162) to handle names containing special characters Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/hooks/use-settings-migration.ts | 2 +- apps/ui/tests/projects/open-existing-project.spec.ts | 2 +- .../test-project-1768743000887/package.json | 4 ++++ .../test-project-1768742910934/package.json | 4 ++++ 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 test/agent-session-test-115699-vyk2nk2/test-project-1768743000887/package.json create mode 100644 test/feature-backlog-test-114171-aysp86y/test-project-1768742910934/package.json diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index 487d23c9..63e62c50 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -469,7 +469,7 @@ export function useSettingsMigration(): MigrationState { // Update localStorage with fresh server data to keep cache in sync // This prevents stale localStorage data from being used when switching between modes try { - localStorage.setItem('automaker-settings-cache', JSON.stringify(serverSettings)); + setItem('automaker-settings-cache', JSON.stringify(serverSettings)); logger.debug('Updated localStorage with fresh server settings'); } catch (storageError) { logger.warn('Failed to update localStorage cache:', storageError); diff --git a/apps/ui/tests/projects/open-existing-project.spec.ts b/apps/ui/tests/projects/open-existing-project.spec.ts index 5018bed5..b89702fb 100644 --- a/apps/ui/tests/projects/open-existing-project.spec.ts +++ b/apps/ui/tests/projects/open-existing-project.spec.ts @@ -159,7 +159,7 @@ test.describe('Open Project', () => { // The project name appears in the project switcher button with title attribute // (The button uses data-testid with projectId, not projectName) if (targetProjectName) { - const projectSwitcherButton = page.locator(`button[title="${targetProjectName}"]`).first(); + const projectSwitcherButton = page.getByRole('button', { name: targetProjectName }).first(); await expect(projectSwitcherButton).toBeVisible({ timeout: 15000, }); diff --git a/test/agent-session-test-115699-vyk2nk2/test-project-1768743000887/package.json b/test/agent-session-test-115699-vyk2nk2/test-project-1768743000887/package.json new file mode 100644 index 00000000..68258c5b --- /dev/null +++ b/test/agent-session-test-115699-vyk2nk2/test-project-1768743000887/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-project-1768743000887", + "version": "1.0.0" +} diff --git a/test/feature-backlog-test-114171-aysp86y/test-project-1768742910934/package.json b/test/feature-backlog-test-114171-aysp86y/test-project-1768742910934/package.json new file mode 100644 index 00000000..4ea81845 --- /dev/null +++ b/test/feature-backlog-test-114171-aysp86y/test-project-1768742910934/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-project-1768742910934", + "version": "1.0.0" +} From f68aee6a197f6a739c29257c1fa4fb7f326a15f3 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 19:29:32 +0530 Subject: [PATCH 18/18] fix: prevent response disposal race condition in E2E test --- .../tests/projects/open-existing-project.spec.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/ui/tests/projects/open-existing-project.spec.ts b/apps/ui/tests/projects/open-existing-project.spec.ts index 51772a25..cd9beb98 100644 --- a/apps/ui/tests/projects/open-existing-project.spec.ts +++ b/apps/ui/tests/projects/open-existing-project.spec.ts @@ -85,7 +85,17 @@ test.describe('Open Project', () => { // AND inject our test project into the projects list await page.route('**/api/settings/global', async (route) => { const response = await route.fetch(); - const json = await response.json(); + // Immediately consume the body to prevent disposal issues + const bodyPromise = response.body(); + const status = response.status(); + const headers = response.headers(); + const body = await bodyPromise; + let json; + try { + json = JSON.parse(body.toString()); + } catch { + json = {}; + } if (json.settings) { // Remove currentProjectId to prevent restoring a project json.settings.currentProjectId = null; @@ -106,8 +116,8 @@ test.describe('Open Project', () => { } } await route.fulfill({ - status: response.status(), - headers: response.headers(), + status: status, + headers: headers, json, }); });