diff --git a/package-lock.json b/package-lock.json index a00f83c2..61e5b7c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,6 +154,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -439,6 +440,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz", "integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -487,6 +489,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -513,6 +516,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -540,6 +544,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", @@ -740,6 +745,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -816,6 +822,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -837,6 +844,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1164,6 +1172,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1550,7 +1559,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1572,7 +1580,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2529,7 +2536,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@lezer/cpp": { "version": "1.1.3", @@ -2569,6 +2577,7 @@ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.3.0" } @@ -2600,6 +2609,7 @@ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", @@ -2622,6 +2632,7 @@ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } @@ -4845,6 +4856,7 @@ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -5002,6 +5014,7 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -5124,6 +5137,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5166,6 +5180,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5176,6 +5191,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5343,6 +5359,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -5719,7 +5736,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/7zip-bin": { "version": "5.2.0", @@ -5754,6 +5772,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6176,6 +6195,7 @@ "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -6310,6 +6330,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -7313,6 +7334,7 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -7389,8 +7411,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -7849,6 +7870,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -7946,8 +7968,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true + "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/dot-prop": { "version": "5.3.0", @@ -8299,7 +8320,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8320,7 +8340,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8336,7 +8355,6 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -8347,7 +8365,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -8610,6 +8627,7 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10208,6 +10226,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -11749,7 +11768,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -13777,7 +13795,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -13795,7 +13812,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -14247,6 +14263,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14256,6 +14273,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14282,6 +14300,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14429,6 +14448,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -14637,7 +14657,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -15958,7 +15979,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -15999,7 +16019,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -16014,7 +16033,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -16119,6 +16137,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16324,6 +16343,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16736,6 +16756,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16827,6 +16848,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 01f713ba..1eca73d9 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -1498,17 +1498,13 @@ app.get( if (status.hasUnencryptedDb) { try { unencryptedSize = fs.statSync(dbPath).size; - } catch (error) { - databaseLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} } if (status.hasEncryptedDb) { try { encryptedSize = fs.statSync(encryptedDbPath).size; - } catch (error) { - databaseLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} } res.json({ diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index 388e008b..afa99bcb 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -908,6 +908,29 @@ router.delete( }, ); + // Notify stats server to stop polling this host + try { + const axios = (await import("axios")).default; + const statsPort = process.env.STATS_PORT || 30005; + await axios.post( + `http://localhost:${statsPort}/host-deleted`, + { hostId: numericHostId }, + { + headers: { + Authorization: req.headers.authorization || "", + Cookie: req.headers.cookie || "", + }, + timeout: 5000, + }, + ); + } catch (err) { + sshLogger.warn("Failed to notify stats server of host deletion", { + operation: "host_delete", + hostId: numericHostId, + error: err instanceof Error ? err.message : String(err), + }); + } + res.json({ message: "SSH host deleted" }); } catch (err) { sshLogger.error("Failed to delete SSH host from database", err, { @@ -1630,6 +1653,39 @@ router.delete( deletedCount: hostsToDelete.length, }); + // Notify stats server to stop polling these hosts + try { + const axios = (await import("axios")).default; + const statsPort = process.env.STATS_PORT || 30005; + for (const host of hostsToDelete) { + try { + await axios.post( + `http://localhost:${statsPort}/host-deleted`, + { hostId: host.id }, + { + headers: { + Authorization: req.headers.authorization || "", + Cookie: req.headers.cookie || "", + }, + timeout: 5000, + }, + ); + } catch (err) { + sshLogger.warn("Failed to notify stats server of host deletion", { + operation: "folder_hosts_delete", + hostId: host.id, + error: err instanceof Error ? err.message : String(err), + }); + } + } + } catch (err) { + sshLogger.warn("Failed to notify stats server of folder deletion", { + operation: "folder_hosts_delete", + folderName, + error: err instanceof Error ? err.message : String(err), + }); + } + res.json({ message: "All hosts in folder deleted successfully", deletedCount: hostsToDelete.length, diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 2e2345e0..11fbdc8e 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -1030,9 +1030,7 @@ router.post("/login", async (req, res) => { if (kekSalt.length === 0) { await authManager.registerUser(userRecord.id, password); } - } catch (error) { - databaseLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} const deviceInfo = parseUserAgent(req); const dataUnlocked = await authManager.authenticateUser( diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 79948efc..dbbdf47f 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -261,6 +261,7 @@ interface SSHSession { isConnected: boolean; lastActive: number; timeout?: NodeJS.Timeout; + activeOperations: number; // Track number of active operations to prevent cleanup mid-operation } interface PendingTOTPSession { @@ -285,11 +286,24 @@ const pendingTOTPSessions: Record = {}; function cleanupSession(sessionId: string) { const session = sshSessions[sessionId]; if (session) { + // Don't cleanup if there are active operations + if (session.activeOperations > 0) { + fileLogger.warn( + `Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`, + { + operation: "cleanup_deferred", + sessionId, + activeOperations: session.activeOperations, + }, + ); + // Reschedule cleanup + scheduleSessionCleanup(sessionId); + return; + } + try { session.client.end(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} clearTimeout(session.timeout); delete sshSessions[sessionId]; } @@ -563,6 +577,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { client, isConnected: true, lastActive: Date.now(), + activeOperations: 0, // Initialize active operations counter }; scheduleSessionCleanup(sessionId); res.json({ status: "success", message: "SSH connection established" }); @@ -917,6 +932,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => { client: session.client, isConnected: true, lastActive: Date.now(), + activeOperations: 0, // Initialize active operations counter }; scheduleSessionCleanup(sessionId); @@ -1060,10 +1076,12 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { } sshConn.lastActive = Date.now(); + sshConn.activeOperations++; // Track operation start const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); sshConn.client.exec(`command ls -la '${escapedPath}'`, (err, stream) => { if (err) { + sshConn.activeOperations--; // Decrement on error fileLogger.error("SSH listFiles error:", err); return res.status(500).json({ error: err.message }); } @@ -1080,6 +1098,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { }); stream.on("close", (code) => { + sshConn.activeOperations--; // Decrement when operation completes if (code !== 0) { fileLogger.error( `SSH listFiles command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 8d292710..2db0d5b4 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -368,9 +368,7 @@ class SSHConnectionPool { if (!conn.inUse && now - conn.lastUsed > maxAge) { try { conn.client.end(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} return false; } return true; @@ -390,9 +388,7 @@ class SSHConnectionPool { for (const conn of connections) { try { conn.client.end(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} } } this.connections.clear(); @@ -430,9 +426,7 @@ class RequestQueue { if (request) { try { await request(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} } } @@ -663,6 +657,7 @@ class PollingManager { async startPollingForHost(host: SSHHostWithCredentials): Promise { const statsConfig = this.parseStatsConfig(host.statsConfig); + const existingConfig = this.pollingConfigs.get(host.id); // Always clear existing timers first @@ -696,6 +691,9 @@ class PollingManager { statsConfig, }; + // Set the config FIRST to prevent race conditions with old timers + this.pollingConfigs.set(host.id, config); + if (statsConfig.statusCheckEnabled) { const intervalMs = statsConfig.statusCheckInterval * 1000; @@ -715,6 +713,7 @@ class PollingManager { if (statsConfig.metricsEnabled) { const intervalMs = statsConfig.metricsInterval * 1000; + // Poll immediately, but only if metrics are actually enabled this.pollHostMetrics(host); config.metricsTimer = setInterval(() => { @@ -728,6 +727,7 @@ class PollingManager { }); } + // Update with the new timers this.pollingConfigs.set(host.id, config); } @@ -750,14 +750,18 @@ class PollingManager { private async pollHostMetrics(host: SSHHostWithCredentials): Promise { // Double-check that metrics are still enabled before collecting + // Always get the LATEST config from the Map, not from the closure const config = this.pollingConfigs.get(host.id); if (!config || !config.statsConfig.metricsEnabled) { return; } + // Use the host from the config to ensure we have the latest version + const currentHost = config.host; + try { - const metrics = await collectMetrics(host); - this.metricsStore.set(host.id, { + const metrics = await collectMetrics(currentHost); + this.metricsStore.set(currentHost.id, { data: metrics, timestamp: Date.now(), }); @@ -765,13 +769,13 @@ class PollingManager { const errorMessage = error instanceof Error ? error.message : String(error); - // Only log errors if metrics collection is actually enabled - // Don't spam logs with errors for hosts that have metrics disabled - if (config.statsConfig.metricsEnabled) { + // Re-check config after the async operation in case it was disabled during collection + const latestConfig = this.pollingConfigs.get(currentHost.id); + if (latestConfig && latestConfig.statsConfig.metricsEnabled) { statsLogger.warn("Failed to collect metrics for host", { operation: "metrics_poll_failed", - hostId: host.id, - hostName: host.name, + hostId: currentHost.id, + hostName: currentHost.name, error: errorMessage, }); } @@ -1330,9 +1334,7 @@ function tcpPing( settled = true; try { socket.destroy(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} resolve(result); }; @@ -1438,6 +1440,34 @@ app.post("/host-updated", async (req, res) => { } }); +app.post("/host-deleted", async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + const { hostId } = req.body; + + if (!SimpleDBOps.isUserDataUnlocked(userId)) { + return res.status(401).json({ + error: "Session expired - please log in again", + code: "SESSION_EXPIRED", + }); + } + + if (!hostId || typeof hostId !== "number") { + return res.status(400).json({ error: "Invalid hostId" }); + } + + try { + pollingManager.stopPollingForHost(hostId, true); + res.json({ message: "Host polling stopped" }); + } catch (error) { + statsLogger.error("Failed to stop polling for host", error, { + operation: "host_deleted", + hostId, + userId, + }); + res.status(500).json({ error: "Failed to stop polling" }); + } +}); + app.get("/metrics/:id", validateHostId, async (req, res) => { const id = Number(req.params.id); const userId = (req as AuthenticatedRequest).userId; diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index b297ab53..f724dd7d 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -323,6 +323,7 @@ wss.on("connection", async (ws: WebSocket, req) => { let isConnecting = false; let isConnected = false; let isCleaningUp = false; + let isShellInitializing = false; // Track shell initialization to prevent cleanup mid-setup ws.on("close", () => { const userWs = userConnections.get(userId); @@ -680,9 +681,11 @@ wss.on("connection", async (ws: WebSocket, req) => { sshConn.on("ready", () => { clearTimeout(connectionTimeout); + // Capture the connection reference immediately and verify it's still valid const conn = sshConn; - if (!conn || isCleaningUp) { + // Additional check: verify the connection is still active before proceeding + if (!conn || isCleaningUp || !sshConn) { sshLogger.warn( "SSH connection was cleaned up before shell could be created", { @@ -692,6 +695,8 @@ wss.on("connection", async (ws: WebSocket, req) => { port, username, isCleaningUp, + connNull: !conn, + sshConnNull: !sshConn, }, ); ws.send( @@ -704,9 +709,30 @@ wss.on("connection", async (ws: WebSocket, req) => { return; } + // Mark that we're initializing the shell to prevent cleanup + isShellInitializing = true; isConnecting = false; isConnected = true; + // Verify connection is still valid right before shell() call + if (!sshConn) { + sshLogger.error( + "SSH connection became null right before shell creation", + { + operation: "ssh_shell", + hostId: id, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: "SSH connection lost during setup", + }), + ); + isShellInitializing = false; + return; + } + conn.shell( { rows: data.rows, @@ -714,6 +740,9 @@ wss.on("connection", async (ws: WebSocket, req) => { term: "xterm-256color", } as PseudoTtyOptions, (err, stream) => { + // Shell initialization complete, clear the flag + isShellInitializing = false; + if (err) { sshLogger.error("Shell error", err, { operation: "ssh_shell", @@ -1235,6 +1264,21 @@ wss.on("connection", async (ws: WebSocket, req) => { if (isCleaningUp) { return; } + + // Don't cleanup if we're in the middle of shell initialization + if (isShellInitializing) { + sshLogger.warn( + "Cleanup attempted during shell initialization, deferring", + { + operation: "cleanup_deferred", + userId, + }, + ); + // Retry cleanup after a short delay + setTimeout(() => cleanupSSH(timeoutId), 100); + return; + } + isCleaningUp = true; if (timeoutId) { @@ -1283,9 +1327,11 @@ wss.on("connection", async (ws: WebSocket, req) => { } function setupPingInterval() { + // More frequent keepalive to prevent idle disconnections pingInterval = setInterval(() => { if (sshConn && sshStream) { try { + // Send null byte as keepalive sshStream.write("\x00"); } catch (e: unknown) { sshLogger.error( @@ -1294,7 +1340,13 @@ wss.on("connection", async (ws: WebSocket, req) => { ); cleanupSSH(); } + } else if (!sshConn || !sshStream) { + // If connection or stream is lost, clear the interval + if (pingInterval) { + clearInterval(pingInterval); + pingInterval = null; + } } - }, 60000); + }, 30000); // Reduced from 60s to 30s for more reliable keepalive } }); diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index 9d584ea2..4365abb8 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -217,9 +217,7 @@ function cleanupTunnelResources( if (verification?.timeout) clearTimeout(verification.timeout); try { verification?.conn.end(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} tunnelVerifications.delete(tunnelName); } @@ -284,9 +282,7 @@ function handleDisconnect( const verification = tunnelVerifications.get(tunnelName); if (verification?.timeout) clearTimeout(verification.timeout); verification?.conn.end(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} tunnelVerifications.delete(tunnelName); } @@ -642,9 +638,7 @@ async function connectSSHTunnel( try { conn.end(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} activeTunnels.delete(tunnelName); @@ -784,9 +778,7 @@ async function connectSSHTunnel( const verification = tunnelVerifications.get(tunnelName); if (verification?.timeout) clearTimeout(verification.timeout); verification?.conn.end(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} tunnelVerifications.delete(tunnelName); } diff --git a/src/backend/starter.ts b/src/backend/starter.ts index f7cd3b4f..b74c9b11 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -21,11 +21,7 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; if (persistentConfig.parsed) { Object.assign(process.env, persistentConfig.parsed); } - } catch (error) { - systemLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} let version = "unknown"; diff --git a/src/backend/utils/auto-ssl-setup.ts b/src/backend/utils/auto-ssl-setup.ts index 6e441df3..acb85d21 100644 --- a/src/backend/utils/auto-ssl-setup.ts +++ b/src/backend/utils/auto-ssl-setup.ts @@ -233,11 +233,7 @@ IP.3 = 0.0.0.0 let envContent = ""; try { envContent = await fs.readFile(this.ENV_FILE, "utf8"); - } catch (error) { - systemLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} let updatedContent = envContent; let hasChanges = false; diff --git a/src/backend/utils/database-file-encryption.ts b/src/backend/utils/database-file-encryption.ts index 8ffb2e1f..f0adc96a 100644 --- a/src/backend/utils/database-file-encryption.ts +++ b/src/backend/utils/database-file-encryption.ts @@ -632,11 +632,7 @@ class DatabaseFileEncryption { try { fs.accessSync(envPath, fs.constants.R_OK); result.environment.envFileReadable = true; - } catch (error) { - databaseLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} } if ( diff --git a/src/backend/utils/lazy-field-encryption.ts b/src/backend/utils/lazy-field-encryption.ts index 8cd3a4d0..ae7ee615 100644 --- a/src/backend/utils/lazy-field-encryption.ts +++ b/src/backend/utils/lazy-field-encryption.ts @@ -82,11 +82,7 @@ export class LazyFieldEncryption { legacyFieldName, ); return decrypted; - } catch (error) { - databaseLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} } const sensitiveFields = [ @@ -178,11 +174,7 @@ export class LazyFieldEncryption { wasPlaintext: false, wasLegacyEncryption: true, }; - } catch (error) { - databaseLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} } return { encrypted: fieldValue, diff --git a/src/backend/utils/ssh-key-utils.ts b/src/backend/utils/ssh-key-utils.ts index 132d3391..8685ec99 100644 --- a/src/backend/utils/ssh-key-utils.ts +++ b/src/backend/utils/ssh-key-utils.ts @@ -85,11 +85,7 @@ function detectKeyTypeFromContent(keyContent: string): string { } else if (decodedString.includes("1.3.101.112")) { return "ssh-ed25519"; } - } catch (error) { - sshLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} if (content.length < 800) { return "ssh-ed25519"; @@ -145,11 +141,7 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string { } else if (decodedString.includes("1.3.101.112")) { return "ssh-ed25519"; } - } catch (error) { - sshLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} if (content.length < 400) { return "ssh-ed25519"; @@ -251,11 +243,7 @@ export function parseSSHKey( useSSH2 = true; } - } catch (error) { - sshLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} } if (!useSSH2) { @@ -281,11 +269,7 @@ export function parseSSHKey( success: true, }; } - } catch (error) { - sshLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} return { privateKey: privateKeyData, diff --git a/src/backend/utils/system-crypto.ts b/src/backend/utils/system-crypto.ts index 41aa2ff1..fdff0263 100644 --- a/src/backend/utils/system-crypto.ts +++ b/src/backend/utils/system-crypto.ts @@ -51,15 +51,7 @@ class SystemCrypto { }, ); } - } catch (fileError) { - // OK: .env file not found or unreadable, will generate new JWT secret - databaseLogger.debug( - ".env file not accessible, will generate new JWT secret", - { - operation: "jwt_env_not_found", - }, - ); - } + } catch (fileError) {} await this.generateAndGuideUser(); } catch (error) { @@ -110,15 +102,7 @@ class SystemCrypto { return; } else { } - } catch (fileError) { - // OK: .env file not found or unreadable, will generate new database key - databaseLogger.debug( - ".env file not accessible, will generate new database key", - { - operation: "db_key_env_not_found", - }, - ); - } + } catch (fileError) {} await this.generateAndGuideDatabaseKey(); } catch (error) { @@ -156,11 +140,7 @@ class SystemCrypto { process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1]; return; } - } catch (error) { - databaseLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} await this.generateAndGuideInternalAuthToken(); } catch (error) { diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index 674dc137..c6cdb3fa 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -915,6 +915,8 @@ "copy": "Kopieren", "cut": "Ausschneiden", "paste": "Einfügen", + "copyPath": "Pfad kopieren", + "copyPaths": "Pfade kopieren", "delete": "Löschen", "properties": "Eigenschaften", "refresh": "Aktualisieren", @@ -924,6 +926,9 @@ "deleteFiles": "{{count}} Element(e) löschen", "filesCopiedToClipboard": "{{count}} Element(e) in die Zwischenablage kopiert", "filesCutToClipboard": "{{count}} Element(e) in die Zwischenablage ausschneiden", + "pathCopiedToClipboard": "Pfad in Zwischenablage kopiert", + "pathsCopiedToClipboard": "{{count}} Pfade in Zwischenablage kopiert", + "failedToCopyPath": "Fehler beim Kopieren des Pfades in die Zwischenablage", "movedItems": "{{count}} Element(e) verschoben", "failedToDeleteItem": "Das Löschen des Elements ist fehlgeschlagen.", "itemRenamedSuccessfully": "{{type}} erfolgreich umbenannt", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 618f9ff5..e9e0632e 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1040,6 +1040,8 @@ "copy": "Copy", "cut": "Cut", "paste": "Paste", + "copyPath": "Copy Path", + "copyPaths": "Copy Paths", "delete": "Delete", "properties": "Properties", "preview": "Preview", @@ -1050,6 +1052,9 @@ "deleteFiles": "Delete {{count}} items", "filesCopiedToClipboard": "{{count}} items copied to clipboard", "filesCutToClipboard": "{{count}} items cut to clipboard", + "pathCopiedToClipboard": "Path copied to clipboard", + "pathsCopiedToClipboard": "{{count}} paths copied to clipboard", + "failedToCopyPath": "Failed to copy path to clipboard", "movedItems": "Moved {{count}} items", "failedToDeleteItem": "Failed to delete item", "itemRenamedSuccessfully": "{{type}} renamed successfully", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index bf689514..7f111fb8 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -910,6 +910,8 @@ "copy": "Copier", "cut": "Couper", "paste": "Coller", + "copyPath": "Copier le chemin", + "copyPaths": "Copier les chemins", "delete": "Supprimer", "properties": "Propriétés", "refresh": "Actualiser", @@ -919,6 +921,9 @@ "deleteFiles": "Supprimer {{count}} éléments", "filesCopiedToClipboard": "{{count}} éléments copiés dans le presse-papiers", "filesCutToClipboard": "{{count}} éléments coupés dans le presse-papiers", + "pathCopiedToClipboard": "Chemin copié dans le presse-papiers", + "pathsCopiedToClipboard": "{{count}} chemins copiés dans le presse-papiers", + "failedToCopyPath": "Échec de la copie du chemin dans le presse-papiers", "movedItems": "{{count}} éléments déplacés", "failedToDeleteItem": "Échec de la suppression de l'élément", "itemRenamedSuccessfully": "{{type}} renommé avec succès", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 594a11fc..9e9350e3 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -861,6 +861,8 @@ "copy": "Copiar", "cut": "Recortar", "paste": "Colar", + "copyPath": "Copiar caminho", + "copyPaths": "Copiar caminhos", "delete": "Excluir", "properties": "Propriedades", "preview": "Visualizar", @@ -871,6 +873,9 @@ "deleteFiles": "Excluir {{count}} itens", "filesCopiedToClipboard": "{{count}} itens copiados para a área de transferência", "filesCutToClipboard": "{{count}} itens recortados para a área de transferência", + "pathCopiedToClipboard": "Caminho copiado para a área de transferência", + "pathsCopiedToClipboard": "{{count}} caminhos copiados para a área de transferência", + "failedToCopyPath": "Falha ao copiar caminho para a área de transferência", "movedItems": "{{count}} itens movidos", "failedToDeleteItem": "Falha ao excluir item", "itemRenamedSuccessfully": "{{type}} renomeado com sucesso", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 2abb1132..0f523deb 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -978,6 +978,8 @@ "copy": "Копировать", "cut": "Вырезать", "paste": "Вставить", + "copyPath": "Копировать путь", + "copyPaths": "Копировать пути", "delete": "Удалить", "properties": "Свойства", "preview": "Просмотр", @@ -988,6 +990,9 @@ "deleteFiles": "Удалить {{count}} элементов", "filesCopiedToClipboard": "{{count}} элементов скопировано в буфер обмена", "filesCutToClipboard": "{{count}} элементов вырезано в буфер обмена", + "pathCopiedToClipboard": "Путь скопирован в буфер обмена", + "pathsCopiedToClipboard": "{{count}} путей скопировано в буфер обмена", + "failedToCopyPath": "Не удалось скопировать путь в буфер обмена", "movedItems": "Перемещено {{count}} элементов", "failedToDeleteItem": "Не удалось удалить элемент", "itemRenamedSuccessfully": "{{type}} успешно переименован", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 8c61b597..3b15347d 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -1025,7 +1025,11 @@ "noSSHConnection": "无SSH连接可用", "enterFolderName": "输入文件夹名称:", "enterFileName": "输入文件名称:", + "copy": "复制", "cut": "剪切", + "paste": "粘贴", + "copyPath": "复制路径", + "copyPaths": "复制路径", "properties": "属性", "refresh": "刷新", "downloadFiles": "下载 {{count}} 个文件", @@ -1034,6 +1038,9 @@ "deleteFiles": "删除 {{count}} 个项目", "filesCopiedToClipboard": "{{count}} 个项目已复制到剪贴板", "filesCutToClipboard": "{{count}} 个项目已剪切到剪贴板", + "pathCopiedToClipboard": "路径已复制到剪贴板", + "pathsCopiedToClipboard": "{{count}} 个路径已复制到剪贴板", + "failedToCopyPath": "复制路径到剪贴板失败", "movedItems": "已移动 {{count}} 个项目", "unknownSize": "未知大小", "fileIsEmpty": "文件为空", diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index 86bb2951..4e7e3926 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -63,6 +63,7 @@ function AppContent() { useEffect(() => { const checkAuth = () => { setAuthLoading(true); + // Don't optimistically set isAuthenticated before checking getUserInfo() .then((meRes) => { if (typeof meRes === "string" || !meRes.username) { @@ -163,13 +164,34 @@ function AppContent() { const showAdmin = currentTabData?.type === "admin"; const showProfile = currentTabData?.type === "user_profile"; + if (authLoading) { + return ( +
+
+
+
+
+ ); + } + return (
- {!isAuthenticated && !authLoading && ( + {!isAuthenticated && (
(null); const [keyDetectionLoading, setKeyDetectionLoading] = useState(false); const keyDetectionTimeoutRef = useRef(null); + const [activeTab, setActiveTab] = useState("general"); + const [formError, setFormError] = useState(null); const [detectedPublicKeyType, setDetectedPublicKeyType] = useState< string | null @@ -60,6 +63,11 @@ export function CredentialEditor({ useState(false); const publicKeyDetectionTimeoutRef = useRef(null); + // Clear error when tab changes + useEffect(() => { + setFormError(null); + }, [activeTab]); + useEffect(() => { const fetchData = async () => { try { @@ -320,6 +328,8 @@ export function CredentialEditor({ const onSubmit = async (data: FormData) => { try { + setFormError(null); + if (!data.name || data.name.trim() === "") { data.name = data.username; } @@ -378,6 +388,28 @@ export function CredentialEditor({ } }; + const handleFormError = () => { + const errors = form.formState.errors; + + if ( + errors.name || + errors.username || + errors.description || + errors.folder || + errors.tags + ) { + setActiveTab("general"); + } else if ( + errors.password || + errors.key || + errors.publicKey || + errors.keyPassword || + errors.keyType + ) { + setActiveTab("authentication"); + } + }; + const [tagInput, setTagInput] = useState(""); const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); @@ -427,11 +459,20 @@ export function CredentialEditor({ >
- + {formError && ( + + {formError} + + )} + {t("credentials.general")} diff --git a/src/ui/desktop/apps/dashboard/Dashboard.tsx b/src/ui/desktop/apps/dashboard/Dashboard.tsx index f0000d9f..e1e63d00 100644 --- a/src/ui/desktop/apps/dashboard/Dashboard.tsx +++ b/src/ui/desktop/apps/dashboard/Dashboard.tsx @@ -204,27 +204,59 @@ export function Dashboard({ setServerStatsLoading(true); const serversWithStats = await Promise.all( - hosts.slice(0, 50).map(async (host: { id: number; name: string }) => { - try { - const metrics = await getServerMetricsById(host.id); - return { - id: host.id, - name: host.name || `Host ${host.id}`, - cpu: metrics.cpu.percent, - ram: metrics.memory.percent, - }; - } catch { - return { - id: host.id, - name: host.name || `Host ${host.id}`, - cpu: null, - ram: null, - }; - } - }), + hosts + .slice(0, 50) + .map( + async (host: { + id: number; + name: string; + statsConfig?: string | { metricsEnabled?: boolean }; + }) => { + try { + // Parse statsConfig if it's a string + let statsConfig: { metricsEnabled?: boolean } = { + metricsEnabled: true, + }; + if (host.statsConfig) { + if (typeof host.statsConfig === "string") { + statsConfig = JSON.parse(host.statsConfig); + } else { + statsConfig = host.statsConfig; + } + } + + // Skip if metrics are disabled + if (statsConfig.metricsEnabled === false) { + return null; + } + + const metrics = await getServerMetricsById(host.id); + return { + id: host.id, + name: host.name || `Host ${host.id}`, + cpu: metrics.cpu.percent, + ram: metrics.memory.percent, + }; + } catch { + return { + id: host.id, + name: host.name || `Host ${host.id}`, + cpu: null, + ram: null, + }; + } + }, + ), ); const validServerStats = serversWithStats.filter( - (server) => server.cpu !== null && server.ram !== null, + ( + server, + ): server is { + id: number; + name: string; + cpu: number | null; + ram: number | null; + } => server !== null && server.cpu !== null && server.ram !== null, ); setServerStats(validServerStats); setServerStatsLoading(false); @@ -339,7 +371,7 @@ export function Dashboard({
) : (
-
-
-
+
+
+
{t("dashboard.title")}
-
-
-

+

+
+

Press LShift twice to open the command palette

{recentActivityLoading ? (
@@ -577,7 +609,7 @@ export function Dashboard({
-
-
+
+

{t("dashboard.quickActions")}

-
+
-
+

{t("dashboard.serverStats")}

{serverStatsLoading ? (
@@ -683,7 +715,7 @@ export function Dashboard({